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, ]; } }