1022 lines
43 KiB
PHP
1022 lines
43 KiB
PHP
<?php
|
|
|
|
namespace App\Filament\Pages\Contabilita;
|
|
|
|
use App\Filament\Pages\Contabilita\CasseBancheMovimenti;
|
|
use App\Models\AiRequest;
|
|
use App\Models\ContoBancario;
|
|
use App\Models\Incasso;
|
|
use App\Models\IncassoAiFeedback;
|
|
use App\Models\MovimentoBancario;
|
|
use App\Models\User;
|
|
use App\Models\GestioneContabile;
|
|
use App\Models\Condomino;
|
|
use App\Modules\Contabilita\Services\IncassoPrimaNotaService;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Columns\BadgeColumn;
|
|
use Filament\Tables\Columns\IconColumn;
|
|
use Filament\Tables\Columns\Summarizers\Sum;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Placeholder;
|
|
use Filament\Notifications\Notification;
|
|
use Illuminate\Database\Eloquent\Builder;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Schema as DbSchema;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Str;
|
|
use Filament\Schemas\Schema as FilamentSchema;
|
|
use App\Models\Soggetto;
|
|
use UnitEnum;
|
|
|
|
class IncassiArchivio extends Page implements HasTable
|
|
{
|
|
use InteractsWithTable;
|
|
|
|
protected function normalizeCodCassa(Incasso $record): string
|
|
{
|
|
$v = (string) ($record->cod_cassa_gescon ?? $record->cod_cassa ?? $record->cod_cassa_legacy ?? '');
|
|
$v = strtoupper(trim($v));
|
|
|
|
return $v;
|
|
}
|
|
|
|
protected function resolveContoBancarioIdFromCodCassa(Incasso $record): ?int
|
|
{
|
|
$cod = $this->normalizeCodCassa($record);
|
|
if ($cod === '') {
|
|
return null;
|
|
}
|
|
|
|
$tenantId = is_string($record->tenant_id ?? null) ? trim((string) $record->tenant_id) : '';
|
|
if ($tenantId === '') {
|
|
return null;
|
|
}
|
|
|
|
$conto = ContoBancario::query()
|
|
->where('tenant_id', $tenantId)
|
|
->whereRaw('UPPER(codice) = ?', [$cod])
|
|
->orderBy('id')
|
|
->first(['id']);
|
|
|
|
return $conto ? (int) $conto->id : null;
|
|
}
|
|
|
|
protected static ?string $navigationLabel = 'Incassi';
|
|
|
|
protected static ?string $title = 'Incassi';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-currency-euro';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Contabilità';
|
|
|
|
protected static ?int $navigationSort = 46;
|
|
|
|
protected static ?string $slug = 'contabilita/incassi';
|
|
|
|
protected string $view = 'filament.pages.contabilita.incassi-archivio';
|
|
|
|
public string $aiRequestsText = '';
|
|
public string $aiResponseText = '';
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->mountInteractsWithTable();
|
|
$this->loadAiRequests();
|
|
$this->loadAiResponse();
|
|
}
|
|
|
|
private function aiRequestsPath(): string
|
|
{
|
|
return base_path('incoming/ai_requests.md');
|
|
}
|
|
|
|
private function aiResponsePath(): string
|
|
{
|
|
return base_path('incoming/ai_responses.md');
|
|
}
|
|
|
|
private function loadAiRequests(): void
|
|
{
|
|
$path = $this->aiRequestsPath();
|
|
if (File::exists($path)) {
|
|
$this->aiRequestsText = (string) File::get($path);
|
|
return;
|
|
}
|
|
|
|
$this->aiRequestsText = '';
|
|
}
|
|
|
|
private function loadAiResponse(): void
|
|
{
|
|
$path = $this->aiResponsePath();
|
|
if (File::exists($path)) {
|
|
$this->aiResponseText = (string) File::get($path);
|
|
return;
|
|
}
|
|
|
|
$this->aiResponseText = $this->defaultAiResponse();
|
|
}
|
|
|
|
public function saveAiResponse(): void
|
|
{
|
|
$path = $this->aiResponsePath();
|
|
File::put($path, $this->aiResponseText);
|
|
Notification::make()->title('Risposta AI salvata')->success()->send();
|
|
}
|
|
|
|
public function saveAiQaToDb(): void
|
|
{
|
|
$requestText = trim($this->aiRequestsText);
|
|
$responseText = trim($this->aiResponseText);
|
|
|
|
if ($requestText === '' && $responseText === '') {
|
|
Notification::make()->title('Nessun contenuto da salvare')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$filters = $this->buildAiFiltersPayload();
|
|
$meta = $this->buildAiMetaPayload();
|
|
|
|
AiRequest::query()->create([
|
|
'context' => 'incassi-archivio',
|
|
'request_text' => $requestText !== '' ? $requestText : '(vuoto)',
|
|
'response_text' => $responseText !== '' ? $responseText : null,
|
|
'filters_json' => $filters !== [] ? $filters : null,
|
|
'meta_json' => $meta !== [] ? $meta : null,
|
|
'status' => $responseText !== '' ? 'answered' : 'open',
|
|
'created_by' => Auth::id(),
|
|
'updated_by' => Auth::id(),
|
|
]);
|
|
|
|
Notification::make()->title('Q/A registrate in tabella')->success()->send();
|
|
}
|
|
|
|
private function defaultAiResponse(): string
|
|
{
|
|
return <<<'MD'
|
|
# Risposte a richieste AI
|
|
|
|
1) **Fonte condomini attivi**
|
|
- Primaria: `persone_unita_relazioni` (attive) → `persone` → `unita_immobiliari`.
|
|
- Legacy fallback: `diritti_reali` → `anagrafica_condominiale` → `unita_immobiliari`.
|
|
|
|
2) **Mapping `rate_emesse.soggetto_responsabile_id`**
|
|
- È collegato a `soggetti.id`.
|
|
- `soggetti.persona_id` → `persone.id` (se presente).
|
|
|
|
3) **Regole rateizzazione**
|
|
- Tabelle: `piani_rateizzazione` (o `piano_rateizzazione`) e `rate_emesse`.
|
|
- Config di stabile: `stabili.rate_ordinarie_mesi`, `stabili.rate_riscaldamento_mesi`, `stabili.descrizione_rate`.
|
|
|
|
4) **Movimenti banca**
|
|
- Descrizione completa: `contabilita_movimenti_banca.descrizione_estesa` (fallback su `descrizione`).
|
|
|
|
5) **Preferenze riconciliazione**
|
|
- Default: allocazione FIFO sulle rate più vecchie (per data_scadenza), con tolleranza importo ±1.00.
|
|
|
|
MD;
|
|
}
|
|
|
|
public function getAiRequestsDbProperty(): array
|
|
{
|
|
if (! Schema::hasTable('ai_requests')) {
|
|
return [];
|
|
}
|
|
|
|
return AiRequest::query()
|
|
->orderByDesc('id')
|
|
->limit(50)
|
|
->get([
|
|
'id',
|
|
'context',
|
|
'status',
|
|
'request_text',
|
|
'response_text',
|
|
'filters_json',
|
|
'meta_json',
|
|
'created_at',
|
|
])
|
|
->map(function (AiRequest $row): array {
|
|
return [
|
|
'id' => $row->id,
|
|
'context' => $row->context,
|
|
'status' => $row->status,
|
|
'request_text' => Str::limit((string) $row->request_text, 140),
|
|
'response_text' => Str::limit((string) $row->response_text, 140),
|
|
'filters_json' => $row->filters_json,
|
|
'meta_json' => $row->meta_json,
|
|
'created_at' => $row->created_at?->format('d/m/Y H:i'),
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function buildAiFiltersPayload(): array
|
|
{
|
|
$filters = [];
|
|
|
|
$anno = $this->getTableFilterState('anno_rif');
|
|
if (! empty($anno)) {
|
|
$filters['anno_rif'] = $anno;
|
|
}
|
|
|
|
$contoId = $this->getTableFilterState('conto_bancario_id');
|
|
if (! empty($contoId)) {
|
|
$filters['conto_bancario_id'] = $contoId;
|
|
}
|
|
|
|
return $filters;
|
|
}
|
|
|
|
private function buildAiMetaPayload(): array
|
|
{
|
|
$user = Auth::user();
|
|
$stabileId = $user instanceof User ? StabileContext::resolveActiveStabileId($user) : null;
|
|
|
|
return array_filter([
|
|
'stabile_id' => $stabileId,
|
|
'user_id' => $user?->id,
|
|
'source' => 'incassi-archivio',
|
|
], fn ($v) => ! is_null($v));
|
|
}
|
|
|
|
protected function resolveCodCol(): ?string
|
|
{
|
|
if (DbSchema::hasColumn('incassi', 'cod_cond_gescon')) {
|
|
return 'cod_cond_gescon';
|
|
}
|
|
if (DbSchema::hasColumn('incassi', 'cod_cond')) {
|
|
return 'cod_cond';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function resolveTipoCol(): ?string
|
|
{
|
|
if (DbSchema::hasColumn('incassi', 'cond_inquil')) {
|
|
return 'cond_inquil';
|
|
}
|
|
if (DbSchema::hasColumn('incassi', 'cond_inq')) {
|
|
return 'cond_inq';
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected function resolveOrderCol(): string
|
|
{
|
|
if (DbSchema::hasColumn('incassi', 'dt_empag')) {
|
|
return 'dt_empag';
|
|
}
|
|
if (DbSchema::hasColumn('incassi', 'data_pagamento')) {
|
|
return 'data_pagamento';
|
|
}
|
|
|
|
return 'id';
|
|
}
|
|
|
|
protected function getTableQuery(): Builder
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return Incasso::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return Incasso::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
$orderCol = $this->resolveOrderCol();
|
|
|
|
$query = Incasso::query()->with(['contoBancario:id,tenant_id,codice,descrizione']);
|
|
|
|
// Per NetGescon NG, l'import incassi salva condominio_id = stabileId.
|
|
if (DbSchema::hasColumn('incassi', 'condominio_id')) {
|
|
$query->where('condominio_id', $stabileId);
|
|
}
|
|
|
|
return $query
|
|
->orderByDesc($orderCol)
|
|
->orderByDesc('id');
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
$codCol = $this->resolveCodCol();
|
|
$tipoCol = $this->resolveTipoCol();
|
|
|
|
$contoMovimentiBase = CasseBancheMovimenti::getUrl(panel: 'admin-filament');
|
|
|
|
return $table
|
|
->striped()
|
|
->columns([
|
|
TextColumn::make('data_pagamento')
|
|
->label('Data')
|
|
->state(function (Incasso $record): ?string {
|
|
$dt = null;
|
|
if (! empty($record->dt_empag)) {
|
|
try {
|
|
$dt = Carbon::parse($record->dt_empag);
|
|
} catch (\Throwable $e) {
|
|
$dt = null;
|
|
}
|
|
}
|
|
if (! $dt && ! empty($record->data_pagamento)) {
|
|
try {
|
|
$dt = Carbon::parse($record->data_pagamento);
|
|
} catch (\Throwable $e) {
|
|
$dt = null;
|
|
}
|
|
}
|
|
|
|
return $dt ? $dt->format('d/m/Y') : null;
|
|
})
|
|
->sortable(query: function (Builder $query, string $direction): Builder {
|
|
// Ordina sempre per colonna più affidabile disponibile (dt_empag, poi data_pagamento).
|
|
if (DbSchema::hasColumn('incassi', 'dt_empag')) {
|
|
return $query->orderBy('dt_empag', $direction);
|
|
}
|
|
if (DbSchema::hasColumn('incassi', 'data_pagamento')) {
|
|
return $query->orderBy('data_pagamento', $direction);
|
|
}
|
|
return $query->orderBy('id', $direction);
|
|
}),
|
|
|
|
TextColumn::make('anno_rif')
|
|
->label('Anno')
|
|
->state(function (Incasso $record): string {
|
|
$dt = null;
|
|
if (! empty($record->dt_empag)) {
|
|
try {
|
|
$dt = Carbon::parse($record->dt_empag);
|
|
} catch (\Throwable $e) {
|
|
$dt = null;
|
|
}
|
|
}
|
|
$dp = null;
|
|
if (! empty($record->data_pagamento)) {
|
|
try {
|
|
$dp = Carbon::parse($record->data_pagamento);
|
|
} catch (\Throwable $e) {
|
|
$dp = null;
|
|
}
|
|
}
|
|
|
|
$annoRaw = $record->anno_rif;
|
|
$annoRawStr = is_null($annoRaw) ? '' : (string) $annoRaw;
|
|
$annoNum = is_numeric($annoRawStr) ? (int) $annoRawStr : null;
|
|
|
|
$annoUmano = null;
|
|
if ($annoNum !== null && $annoNum >= 1900) {
|
|
$annoUmano = $annoNum;
|
|
} elseif ($dt) {
|
|
$annoUmano = (int) $dt->format('Y');
|
|
} elseif ($dp) {
|
|
$annoUmano = (int) $dp->format('Y');
|
|
}
|
|
|
|
$annoLabel = $annoUmano ? (string) $annoUmano : ($annoRawStr !== '' ? ltrim($annoRawStr, '0') : '—');
|
|
if ($annoUmano && $annoNum !== null && $annoNum > 0 && $annoNum < 1900) {
|
|
$annoLabel .= ' (' . str_pad((string) $annoNum, 4, '0', STR_PAD_LEFT) . ')';
|
|
}
|
|
|
|
return $annoLabel;
|
|
})
|
|
->sortable(),
|
|
|
|
TextColumn::make('tipo')
|
|
->label('C/I')
|
|
->state(function (Incasso $record) use ($tipoCol): ?string {
|
|
if (! $tipoCol) {
|
|
return null;
|
|
}
|
|
$v = (string) (data_get($record, $tipoCol) ?? '');
|
|
$v = strtoupper(trim($v));
|
|
return $v !== '' ? $v : null;
|
|
})
|
|
->toggleable(),
|
|
|
|
TextColumn::make('nominativo')
|
|
->label('Nominativo')
|
|
->state(function (Incasso $record): ?string {
|
|
$meta = is_array($record->metadati_gescon ?? null) ? $record->metadati_gescon : [];
|
|
$v = (string) ($meta['nominativo'] ?? '');
|
|
$v = trim($v);
|
|
if ($v !== '') {
|
|
return $v;
|
|
}
|
|
|
|
if ($record->condomino) {
|
|
$nome = trim((string) ($record->condomino->nome ?? ''));
|
|
$cognome = trim((string) ($record->condomino->cognome ?? ''));
|
|
$full = trim($nome . ' ' . $cognome);
|
|
if ($full !== '') {
|
|
return $full;
|
|
}
|
|
}
|
|
|
|
if (! empty($record->condomino_id)) {
|
|
$s = Soggetto::query()->find((int) $record->condomino_id);
|
|
if ($s) {
|
|
$rs = trim((string) ($s->ragione_sociale ?? ''));
|
|
if ($rs !== '') {
|
|
return $rs;
|
|
}
|
|
$nome = trim((string) ($s->nome ?? ''));
|
|
$cognome = trim((string) ($s->cognome ?? ''));
|
|
$full = trim($nome . ' ' . $cognome);
|
|
if ($full !== '') {
|
|
return $full;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
})
|
|
->searchable(query: function (Builder $query, string $search): Builder {
|
|
$s = trim($search);
|
|
if ($s === '') {
|
|
return $query;
|
|
}
|
|
// JSON search (MySQL): metadati_gescon->$.nominativo
|
|
if (DbSchema::hasColumn('incassi', 'metadati_gescon')) {
|
|
return $query->whereRaw("JSON_UNQUOTE(JSON_EXTRACT(metadati_gescon, '$.nominativo')) like ?", ['%' . $s . '%']);
|
|
}
|
|
return $query;
|
|
})
|
|
->toggleable(),
|
|
|
|
TextColumn::make('cod_cond')
|
|
->label('Cod. cond')
|
|
->state(function (Incasso $record) use ($codCol): ?string {
|
|
if (! $codCol) {
|
|
return null;
|
|
}
|
|
$v = (string) (data_get($record, $codCol) ?? '');
|
|
$v = trim($v);
|
|
return $v !== '' ? $v : null;
|
|
})
|
|
->searchable(query: function (Builder $query, string $search) use ($codCol): Builder {
|
|
$s = trim($search);
|
|
if ($s === '' || ! $codCol) {
|
|
return $query;
|
|
}
|
|
return $query->where($codCol, 'like', '%' . $s . '%');
|
|
})
|
|
->toggleable(),
|
|
|
|
TextColumn::make('importo_pagato_euro')
|
|
->label('Importo')
|
|
->state(function (Incasso $record): float {
|
|
$val = $record->importo_pagato_euro ?? $record->importo_pagato ?? 0;
|
|
return is_numeric($val) ? (float) $val : 0.0;
|
|
})
|
|
->formatStateUsing(fn(float $state): string => '€ ' . number_format($state, 2, ',', '.'))
|
|
->summarize(
|
|
Sum::make()
|
|
->label('Totale')
|
|
->formatStateUsing(fn($state): string => '€ ' . number_format((float) $state, 2, ',', '.'))
|
|
)
|
|
->alignEnd()
|
|
->sortable(),
|
|
|
|
TextColumn::make('descrizione')
|
|
->label('Descrizione')
|
|
->wrap()
|
|
->searchable(),
|
|
|
|
TextColumn::make('cod_cassa')
|
|
->label('Cod. cassa')
|
|
->state(function (Incasso $record): ?string {
|
|
$v = $this->normalizeCodCassa($record);
|
|
if ($v === '') {
|
|
return null;
|
|
}
|
|
|
|
$contoCode = $record->contoBancario && is_string($record->contoBancario->codice)
|
|
? strtoupper(trim((string) $record->contoBancario->codice))
|
|
: '';
|
|
if ($contoCode !== '' && $contoCode !== $v) {
|
|
return $v . ' (≠ ' . $contoCode . ')';
|
|
}
|
|
|
|
return $v;
|
|
})
|
|
->toggleable(),
|
|
|
|
TextColumn::make('conto_bancario_codice')
|
|
->label('Banca (codice)')
|
|
->state(function (Incasso $record): ?string {
|
|
$cod = $record->contoBancario && is_string($record->contoBancario->codice)
|
|
? trim((string) $record->contoBancario->codice)
|
|
: '';
|
|
return $cod !== '' ? $cod : null;
|
|
})
|
|
->toggleable(isToggledHiddenByDefault: true),
|
|
|
|
IconColumn::make('semaforo')
|
|
->label('')
|
|
->icon('heroicon-s-circle')
|
|
->color(function (Incasso $record): string {
|
|
$stato = strtolower(trim((string) ($record->stato_riconciliazione ?? '')));
|
|
$conf = is_numeric($record->confidenza_match ?? null) ? (float) $record->confidenza_match : null;
|
|
$hasMov = ! empty($record->movimento_bancario_id);
|
|
|
|
if ($stato === 'anomala') {
|
|
return 'danger';
|
|
}
|
|
|
|
if ($hasMov || $stato === 'riconciliato' || $stato === 'automatica' || $stato === 'manuale') {
|
|
return 'success';
|
|
}
|
|
|
|
if ($conf !== null && $conf >= 0.7) {
|
|
return 'warning';
|
|
}
|
|
|
|
return 'warning';
|
|
})
|
|
->alignCenter(),
|
|
|
|
IconColumn::make('conto_bancario_id')
|
|
->label('Banca')
|
|
->icon(fn(Incasso $record): ?string => !empty($record->conto_bancario_id) ? 'heroicon-o-building-library' : null)
|
|
->color(fn(Incasso $record): string => !empty($record->conto_bancario_id) ? 'gray' : 'secondary')
|
|
->url(function (Incasso $record) use ($contoMovimentiBase): ?string {
|
|
$contoId = is_numeric($record->conto_bancario_id ?? null) ? (int) $record->conto_bancario_id : null;
|
|
return $contoId ? ($contoMovimentiBase . '?conto_id=' . $contoId) : null;
|
|
})
|
|
->openUrlInNewTab()
|
|
->toggleable(),
|
|
])
|
|
->actions([
|
|
Action::make('allinea_banca_da_cod_cassa')
|
|
->label('Allinea banca da Cod. cassa')
|
|
->icon('heroicon-o-arrow-path')
|
|
->visible(function (Incasso $record): bool {
|
|
$cod = $this->normalizeCodCassa($record);
|
|
if ($cod === '') {
|
|
return false;
|
|
}
|
|
$contoCode = $record->contoBancario && is_string($record->contoBancario->codice)
|
|
? strtoupper(trim((string) $record->contoBancario->codice))
|
|
: '';
|
|
return $contoCode === '' || $contoCode !== $cod;
|
|
})
|
|
->action(function (Incasso $record): void {
|
|
$targetId = $this->resolveContoBancarioIdFromCodCassa($record);
|
|
if (! $targetId) {
|
|
Notification::make()->title('Nessuna banca trovata')->body('Impossibile risolvere il conto per questo Cod. cassa.')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$record->conto_bancario_id = $targetId;
|
|
$record->save();
|
|
|
|
Notification::make()->title('Banca aggiornata')->success()->send();
|
|
$this->resetTable();
|
|
}),
|
|
|
|
Action::make('genera_prima_nota')
|
|
->label('Genera prima nota')
|
|
->icon('heroicon-o-book-open')
|
|
->visible(function (Incasso $record): bool {
|
|
if (! DbSchema::hasTable('contabilita_registrazioni') || ! DbSchema::hasTable('contabilita_movimenti')) {
|
|
return false;
|
|
}
|
|
if (! DbSchema::hasColumn('incassi', 'registrazione_id')) {
|
|
return false;
|
|
}
|
|
return empty($record->registrazione_id);
|
|
})
|
|
->form([
|
|
Select::make('gestione_id')
|
|
->label('Gestione')
|
|
->options(function (): array {
|
|
if (! DB::getSchemaBuilder()->hasTable('gestioni_contabili')) {
|
|
return [];
|
|
}
|
|
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
return GestioneContabile::query()
|
|
->where('stabile_id', $stabileId)
|
|
->orderByDesc('anno_gestione')
|
|
->orderBy('tipo_gestione')
|
|
->orderBy('numero_straordinaria')
|
|
->get(['id', 'denominazione', 'tipo_gestione', 'anno_gestione', 'numero_straordinaria'])
|
|
->mapWithKeys(fn (GestioneContabile $g) => [
|
|
(string) $g->id => $g->denominazione
|
|
. ' (' . $g->tipo_gestione . ' ' . $g->anno_gestione
|
|
. ($g->numero_straordinaria ? ' #' . $g->numero_straordinaria : '')
|
|
. ')',
|
|
])
|
|
->all();
|
|
})
|
|
->searchable()
|
|
->visible(fn (Incasso $record) => empty($record->gestione_id))
|
|
->required(fn (Incasso $record) => empty($record->gestione_id)),
|
|
|
|
Select::make('modalita_incasso')
|
|
->label('Modalità incasso')
|
|
->options([
|
|
'contanti' => 'Contanti',
|
|
'carta' => 'Carta/POS',
|
|
'bonifico' => 'Bonifico',
|
|
'cBill' => 'CBILL',
|
|
'mav' => 'MAV',
|
|
'sdd' => 'SDD/Addebito diretto',
|
|
'assegno' => 'Assegno',
|
|
'altro' => 'Altro',
|
|
])
|
|
->default(fn (Incasso $record): ?string => DbSchema::hasColumn('incassi', 'modalita_incasso')
|
|
? (is_string($record->modalita_incasso ?? null) ? (string) $record->modalita_incasso : null)
|
|
: null)
|
|
->searchable()
|
|
->nullable(),
|
|
|
|
Select::make('condomino_id')
|
|
->label('Pagante (condomino / inquilino / avente diritto)')
|
|
->helperText('Se non selezionato, verrà usato il conto crediti generico (se configurato).')
|
|
->options(function (Incasso $record): array {
|
|
$q = Condomino::query();
|
|
|
|
if (Schema::hasColumn('condomini', 'tenant_id') && ! empty($record->tenant_id)) {
|
|
$q->where('tenant_id', (string) $record->tenant_id);
|
|
}
|
|
|
|
// Se disponibile, filtriamo per condominio/stabile Gescon.
|
|
if (Schema::hasColumn('condomini', 'cod_condominio_gescon') && ! empty($record->cod_cond_gescon)) {
|
|
$q->where('cod_condominio_gescon', (string) $record->cod_cond_gescon);
|
|
}
|
|
|
|
// Se disponibile, filtriamo per tipologia (inquilino/proprietario)
|
|
if (Schema::hasColumn('condomini', 'inquilino_proprietario') && ! empty($record->cond_inquil)) {
|
|
$q->where('inquilino_proprietario', (string) $record->cond_inquil);
|
|
}
|
|
|
|
// Ordinamento “best effort”
|
|
if (Schema::hasColumn('condomini', 'cognome')) {
|
|
$q->orderBy('cognome');
|
|
}
|
|
if (Schema::hasColumn('condomini', 'nome')) {
|
|
$q->orderBy('nome');
|
|
}
|
|
|
|
return $q
|
|
->limit(500)
|
|
->get()
|
|
->mapWithKeys(fn (Condomino $c) => [
|
|
(string) $c->getKey() => $c->display_name,
|
|
])
|
|
->all();
|
|
})
|
|
->searchable()
|
|
->default(fn (Incasso $record): ?int => ! empty($record->condomino_id) ? (int) $record->condomino_id : null)
|
|
->nullable(),
|
|
])
|
|
->action(function (Incasso $record, array $data): void {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
Notification::make()->title('Utente non valido')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
// Best-effort: se Cod. cassa (legacy) non coincide col conto associato, riallinea.
|
|
try {
|
|
$cod = $this->normalizeCodCassa($record);
|
|
$contoCode = $record->contoBancario && is_string($record->contoBancario->codice)
|
|
? strtoupper(trim((string) $record->contoBancario->codice))
|
|
: '';
|
|
if ($cod !== '' && ($contoCode === '' || $contoCode !== $cod)) {
|
|
$targetId = $this->resolveContoBancarioIdFromCodCassa($record);
|
|
if ($targetId) {
|
|
$record->conto_bancario_id = $targetId;
|
|
$record->save();
|
|
$record->refresh();
|
|
}
|
|
}
|
|
} catch (\Throwable) {
|
|
// ignore
|
|
}
|
|
|
|
try {
|
|
$service = app(IncassoPrimaNotaService::class);
|
|
$service->generaRegistrazioneDaIncasso($user, $record, [
|
|
'gestione_id' => (empty($record->gestione_id) && isset($data['gestione_id']) && is_numeric($data['gestione_id']))
|
|
? (int) $data['gestione_id']
|
|
: (int) ($record->gestione_id ?? 0),
|
|
'modalita_incasso' => isset($data['modalita_incasso']) && is_string($data['modalita_incasso'])
|
|
? (string) $data['modalita_incasso']
|
|
: null,
|
|
'condomino_id' => isset($data['condomino_id']) && is_numeric($data['condomino_id'])
|
|
? (int) $data['condomino_id']
|
|
: null,
|
|
]);
|
|
Notification::make()->title('Prima nota generata')->success()->send();
|
|
$this->resetTable();
|
|
} catch (\Throwable $e) {
|
|
Notification::make()->title('Errore')->body($e->getMessage())->danger()->send();
|
|
}
|
|
}),
|
|
|
|
Action::make('dettaglio_nominativo')
|
|
->label('Dettaglio nominativo')
|
|
->icon('heroicon-o-identification')
|
|
->modalHeading('Dettaglio nominativo / estratto')
|
|
->modalContent(function (Incasso $record) {
|
|
$data = $this->buildNominativoEstrattoData($record);
|
|
return view('filament.pages.contabilita.incassi-nominativo-modal', $data);
|
|
})
|
|
->modalSubmitAction(false)
|
|
->modalCancelActionLabel('Chiudi'),
|
|
|
|
Action::make('ai_feedback')
|
|
->label('AI feedback')
|
|
->icon('heroicon-o-cpu-chip')
|
|
->visible(fn () => Schema::hasTable('incassi_ai_feedback'))
|
|
->form([
|
|
Select::make('ai_status')
|
|
->label('Stato AI')
|
|
->options([
|
|
'pending' => 'In attesa',
|
|
'suggested' => 'Suggerito',
|
|
'accepted' => 'Accettato',
|
|
'rejected' => 'Respinto',
|
|
])
|
|
->required(),
|
|
TextInput::make('ai_confidenza')
|
|
->label('Confidenza')
|
|
->numeric()
|
|
->step(0.01)
|
|
->minValue(0)
|
|
->maxValue(1)
|
|
->nullable(),
|
|
TextInput::make('movimento_bancario_id')
|
|
->label('Movimento bancario ID')
|
|
->numeric()
|
|
->nullable(),
|
|
TextInput::make('registrazione_id')
|
|
->label('Registrazione prima nota ID')
|
|
->numeric()
|
|
->nullable(),
|
|
Textarea::make('ai_note')
|
|
->label('Note AI')
|
|
->rows(3)
|
|
->nullable(),
|
|
Textarea::make('user_feedback')
|
|
->label('Feedback operatore')
|
|
->rows(3)
|
|
->nullable(),
|
|
Placeholder::make('ai_payload')
|
|
->label('Payload AI')
|
|
->content(fn (Incasso $record): string => $this->getAiPayloadSummary($record)),
|
|
])
|
|
->mountUsing(function (FilamentSchema $form, Incasso $record): void {
|
|
$form->fill($this->getAiFeedbackFormData($record));
|
|
})
|
|
->action(function (Incasso $record, array $data): void {
|
|
$this->saveAiFeedback($record, $data);
|
|
Notification::make()->title('Feedback AI salvato')->success()->send();
|
|
}),
|
|
])
|
|
->filters([
|
|
SelectFilter::make('anno_rif')
|
|
->label('Anno')
|
|
->options(function (): array {
|
|
$user = Auth::user();
|
|
if (!$user instanceof User) {
|
|
return [];
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (!$stabileId) {
|
|
return [];
|
|
}
|
|
|
|
$q = Incasso::query();
|
|
if (DbSchema::hasColumn('incassi', 'condominio_id')) {
|
|
$q->where('condominio_id', $stabileId);
|
|
}
|
|
|
|
// In Gescon, anno_rif è spesso 0001, 0002, 0003... e non coincide con l'anno calendario.
|
|
if (! DbSchema::hasColumn('incassi', 'anno_rif')) {
|
|
return [];
|
|
}
|
|
|
|
$years = $q->selectRaw('DISTINCT anno_rif as y')
|
|
->whereNotNull('anno_rif')
|
|
->orderByDesc('y')
|
|
->pluck('y')
|
|
->filter(fn ($v) => is_numeric($v) && (int) $v > 0)
|
|
->values()
|
|
->all();
|
|
|
|
$out = [];
|
|
foreach ($years as $y) {
|
|
$yy = (int) $y;
|
|
$out[(string) $yy] = str_pad((string) $yy, 4, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
return $out;
|
|
})
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$year = isset($data['value']) ? (int) $data['value'] : 0;
|
|
if ($year <= 0) {
|
|
return $query;
|
|
}
|
|
if (DbSchema::hasColumn('incassi', 'anno_rif')) {
|
|
return $query->where('anno_rif', $year);
|
|
}
|
|
return $query;
|
|
}),
|
|
|
|
SelectFilter::make('conto_bancario_id')
|
|
->label('Conto corrente')
|
|
->searchable()
|
|
->options(function (): array {
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return [];
|
|
}
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return [];
|
|
}
|
|
|
|
$q = Incasso::query();
|
|
if (DbSchema::hasColumn('incassi', 'condominio_id')) {
|
|
$q->where('condominio_id', $stabileId);
|
|
}
|
|
|
|
$tenantIds = $q->selectRaw('DISTINCT tenant_id as t')
|
|
->whereNotNull('tenant_id')
|
|
->pluck('t')
|
|
->filter(fn($v) => is_string($v) && trim($v) !== '')
|
|
->map(fn($v) => trim((string) $v))
|
|
->values()
|
|
->all();
|
|
|
|
if (! is_array($tenantIds) || count($tenantIds) === 0) {
|
|
return [];
|
|
}
|
|
|
|
return ContoBancario::query()
|
|
->whereIn('tenant_id', $tenantIds)
|
|
->orderBy('codice')
|
|
->orderBy('descrizione')
|
|
->get(['id', 'codice', 'descrizione'])
|
|
->mapWithKeys(function (ContoBancario $c): array {
|
|
$code = trim((string) ($c->codice ?? ''));
|
|
$desc = trim((string) ($c->descrizione ?? ''));
|
|
$label = $code !== '' ? $code : ('Conto #' . $c->id);
|
|
if ($desc !== '') {
|
|
$label .= ' — ' . $desc;
|
|
}
|
|
return [(string) $c->id => $label];
|
|
})
|
|
->all();
|
|
})
|
|
->query(function (Builder $query, array $data): Builder {
|
|
$id = isset($data['value']) ? (int) $data['value'] : 0;
|
|
return $id > 0 ? $query->where('conto_bancario_id', $id) : $query;
|
|
}),
|
|
])
|
|
->defaultPaginationPageOption(50);
|
|
}
|
|
|
|
private function getAiFeedbackFormData(Incasso $record): array
|
|
{
|
|
$feedback = IncassoAiFeedback::query()
|
|
->where('incasso_id', $record->id)
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
return [
|
|
'ai_status' => $feedback?->ai_status ?? 'pending',
|
|
'ai_confidenza' => $feedback?->ai_confidenza,
|
|
'movimento_bancario_id' => $feedback?->movimento_bancario_id ?? $record->movimento_bancario_id,
|
|
'registrazione_id' => $feedback?->registrazione_id ?? $record->registrazione_id,
|
|
'ai_note' => $feedback?->ai_note,
|
|
'user_feedback' => $feedback?->user_feedback,
|
|
];
|
|
}
|
|
|
|
private function getAiPayloadSummary(Incasso $record): string
|
|
{
|
|
$feedback = IncassoAiFeedback::query()
|
|
->where('incasso_id', $record->id)
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
$payload = $feedback?->ai_payload;
|
|
if (empty($payload)) {
|
|
return '—';
|
|
}
|
|
|
|
return is_array($payload) ? json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $payload;
|
|
}
|
|
|
|
private function saveAiFeedback(Incasso $record, array $data): void
|
|
{
|
|
$user = Auth::user();
|
|
$feedback = IncassoAiFeedback::query()
|
|
->firstOrNew(['incasso_id' => $record->id]);
|
|
|
|
$feedback->fill([
|
|
'ai_status' => $data['ai_status'] ?? 'pending',
|
|
'ai_confidenza' => $data['ai_confidenza'] ?? null,
|
|
'movimento_bancario_id' => $data['movimento_bancario_id'] ?? null,
|
|
'registrazione_id' => $data['registrazione_id'] ?? null,
|
|
'ai_note' => $data['ai_note'] ?? null,
|
|
'user_feedback' => $data['user_feedback'] ?? null,
|
|
'updated_by' => $user?->id,
|
|
]);
|
|
|
|
if (! $feedback->exists) {
|
|
$feedback->created_by = $user?->id;
|
|
}
|
|
|
|
$feedback->save();
|
|
}
|
|
|
|
private function buildNominativoEstrattoData(Incasso $record): array
|
|
{
|
|
$condomino = $record->condomino;
|
|
$movimento = null;
|
|
if (! empty($record->movimento_bancario_id)) {
|
|
$movimento = MovimentoBancario::query()->find($record->movimento_bancario_id);
|
|
}
|
|
|
|
$incassi = [];
|
|
if (! empty($record->condomino_id)) {
|
|
$incassi = Incasso::query()
|
|
->where('condomino_id', $record->condomino_id)
|
|
->orderByDesc($this->resolveOrderCol())
|
|
->limit(200)
|
|
->get();
|
|
}
|
|
|
|
$rateEmesse = [];
|
|
if (Schema::hasTable('rate_emesse')) {
|
|
$rateQuery = DB::table('rate_emesse');
|
|
if (Schema::hasColumn('rate_emesse', 'condomino_id') && ! empty($record->condomino_id)) {
|
|
$rateQuery->where('condomino_id', $record->condomino_id);
|
|
} elseif (Schema::hasColumn('rate_emesse', 'soggetto_responsabile_id') && Schema::hasColumn('condomini', 'soggetto_responsabile_id') && ! empty($record->condomino_id)) {
|
|
$soggettoId = DB::table('condomini')->where('id', $record->condomino_id)->value('soggetto_responsabile_id');
|
|
if ($soggettoId) {
|
|
$rateQuery->where('soggetto_responsabile_id', $soggettoId);
|
|
} else {
|
|
$rateQuery = null;
|
|
}
|
|
}
|
|
|
|
if ($rateQuery) {
|
|
$rateEmesse = $rateQuery
|
|
->orderByDesc('data_scadenza')
|
|
->limit(200)
|
|
->get();
|
|
}
|
|
}
|
|
|
|
return [
|
|
'record' => $record,
|
|
'condomino' => $condomino,
|
|
'movimento' => $movimento,
|
|
'incassi' => $incassi,
|
|
'rateEmesse' => $rateEmesse,
|
|
'registrazione_id' => $record->registrazione_id,
|
|
];
|
|
}
|
|
}
|