netgescon-day0/app/Filament/Pages/Contabilita/IncassiArchivio.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,
];
}
}