*/ public array $unitaList = []; /** @var array> */ public array $rows = []; /** @var array>,totali:array{dovuto:float,pagato:float,residuo:float}}> */ public array $groups = []; /** @var array> */ public array $incassi = []; /** @var array{addebitato:float,pagato:float,residuo:float,rate:int} */ public array $totali = [ 'addebitato' => 0.0, 'pagato' => 0.0, 'residuo' => 0.0, 'rate' => 0, ]; /** @var array{addebitato:float,pagato:float,residuo:float,rate:int} */ public array $totaliUnita = [ 'addebitato' => 0.0, 'pagato' => 0.0, 'residuo' => 0.0, 'rate' => 0, ]; public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public function mount(int|string $record): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make() ->title('Stabile non selezionato') ->body('Seleziona uno stabile per consultare l\'estratto conto.') ->warning() ->send(); return; } $req = request(); $vista = trim((string) $req->query('vista', 'unita')); $this->vista = in_array($vista, ['unita', 'soggetto'], true) ? $vista : 'unita'; $this->qUnita = trim((string) $req->query('q', '')); $this->showScadenza = $req->boolean('scadenza'); $unitaIdRaw = $req->query('unita_id'); $this->unitaId = is_numeric($unitaIdRaw) ? (int) $unitaIdRaw : null; if ($this->unitaId === null) { try { $ref = (string) ($req->headers->get('referer') ?? ''); if ($ref !== '') { $q = parse_url($ref, PHP_URL_QUERY); if (is_string($q) && $q !== '') { parse_str($q, $params); $refUnita = $params['unita_id'] ?? null; if (is_numeric($refUnita)) { $this->unitaId = (int) $refUnita; } } } } catch (\Throwable $e) { // ignore } } $this->soggetto = Soggetto::query()->findOrFail((int) $record); if (! DbSchema::hasTable('rate_emesse')) { return; } $query = RataEmessaNg::query() ->where('soggetto_responsabile_id', (int) $this->soggetto->id) ->whereHas('unitaImmobiliare', function ($q) use ($stabileId): void { $q->where('stabile_id', (int) $stabileId); }) ->with([ 'unitaImmobiliare:id,stabile_id,codice_unita,denominazione,scala,piano,interno', 'pianoRateizzazione:id,stabile_id,descrizione', ]) ->orderBy('data_emissione') ->orderBy('id') ->limit(5000); $annoGestione = AnnoGestioneContext::resolveActiveAnno($user); $codStabile = $user instanceof User ? (StabileContext::getActiveStabile($user)?->codice_stabile ?? '') : ''; if ($annoGestione && $codStabile && DbSchema::connection('gescon_import')->hasTable('rate_emissioni')) { $legacyAnni = DB::connection('gescon_import')->table('rate_emissioni') ->where('cod_stabile', (string) $codStabile) ->whereNotNull('anno_emissione') ->whereYear('data_emissione', (int) $annoGestione) ->pluck('anno_emissione') ->map(fn($v) => trim((string) $v)) ->filter(fn($v) => $v !== '') ->unique() ->values() ->all(); if (!empty($legacyAnni)) { $query->where(function ($q) use ($legacyAnni): void { foreach ($legacyAnni as $legacyAnno) { $q->orWhereRaw( "SUBSTRING_INDEX(SUBSTRING_INDEX(note, 'anno_emissione=', -1), '|', 1) = ?", [$legacyAnno] ); } }); } } $items = $query->get(); // Totali soggetto (tutte le unità) $this->totali['rate'] = (int) $items->count(); $this->totali['addebitato'] = (float) $items->sum(fn (RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto); $this->totali['pagato'] = (float) $items->sum(fn (RataEmessaNg $r) => (float) $r->importo_pagato); $this->totali['residuo'] = (float) ($this->totali['addebitato'] - $this->totali['pagato']); // Unità disponibili per questo soggetto $unitaMap = []; foreach ($items as $r) { $u = $r->unitaImmobiliare; if (! ($u instanceof UnitaImmobiliare)) { continue; } $unitaMap[$u->id] = $u; } $unitaAll = array_values($unitaMap); usort($unitaAll, function (UnitaImmobiliare $a, UnitaImmobiliare $b): int { $ak = (string) ($a->codice_unita ?? ''); $bk = (string) ($b->codice_unita ?? ''); if ($ak !== '' && $bk !== '') { return strcmp($ak, $bk); } return $a->id <=> $b->id; }); $unitaFiltered = $unitaAll; if ($this->qUnita !== '') { $needle = mb_strtolower($this->qUnita); $unitaFiltered = array_values(array_filter($unitaFiltered, function (UnitaImmobiliare $u) use ($needle): bool { $hay = mb_strtolower( trim((string) ($u->codice_unita ?? '') . ' ' . (string) ($u->interno ?? '') . ' ' . (string) ($u->denominazione ?? '')) ); return $hay !== '' && str_contains($hay, $needle); })); } if ($this->unitaId === null || ! array_key_exists($this->unitaId, $unitaMap)) { $this->unitaId = ! empty($unitaFiltered) ? (int) $unitaFiltered[0]->id : (! empty($unitaAll) ? (int) $unitaAll[0]->id : null); } $this->unitaList = []; foreach ($unitaFiltered as $u) { $short = $this->formatUnitaShort($u); $tot = (float) $items->where('unita_immobiliare_id', $u->id)->sum(fn (RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto); $pag = (float) $items->where('unita_immobiliare_id', $u->id)->sum(fn (RataEmessaNg $r) => (float) $r->importo_pagato); $this->unitaList[] = [ 'id' => (int) $u->id, 'label' => $this->formatUnitaLabel($u), 'short' => $short, 'totale' => $tot, 'residuo' => $tot - $pag, ]; } $this->prevUnitaId = null; $this->nextUnitaId = null; if ($this->unitaId !== null && ! empty($this->unitaList)) { $idx = null; foreach ($this->unitaList as $i => $u) { if ((int) $u['id'] === (int) $this->unitaId) { $idx = $i; break; } } if ($idx !== null) { $this->prevUnitaId = $idx > 0 ? (int) $this->unitaList[$idx - 1]['id'] : null; $this->nextUnitaId = $idx < (count($this->unitaList) - 1) ? (int) $this->unitaList[$idx + 1]['id'] : null; } } // Determina se esistono quote inquilino/condomino per la stessa unità (per link rapido) if ($this->unitaId !== null) { $baseOther = RataEmessaNg::query() ->where('unita_immobiliare_id', (int) $this->unitaId) ->whereHas('unitaImmobiliare', function ($q) use ($stabileId): void { $q->where('stabile_id', (int) $stabileId); }) ->whereNotNull('note'); $condIds = (clone $baseOther) ->where('note', 'like', '%tipo_soggetto=C%') ->distinct() ->pluck('soggetto_responsabile_id') ->map(fn ($v) => (int) $v) ->values(); $inqIds = (clone $baseOther) ->where('note', 'like', '%tipo_soggetto=I%') ->distinct() ->pluck('soggetto_responsabile_id') ->map(fn ($v) => (int) $v) ->values(); $this->recordCondominoId = $condIds->count() === 1 ? (int) $condIds[0] : null; $this->recordInquilinoId = $inqIds->count() === 1 ? (int) $inqIds[0] : null; } $aggregateItems = function ($collection) { return $collection ->groupBy(function (RataEmessaNg $r): string { return (int) ($r->unita_immobiliare_id ?? 0) . '|' . (int) ($r->piano_rateizzazione_id ?? 0) . '|' . (int) ($r->numero_rata_progressivo ?? 0); }) ->map(function ($group) { /** @var RataEmessaNg $first */ $first = $group->first(); if ($group->count() > 1) { $first->importo_addebitato_soggetto = $group->sum('importo_addebitato_soggetto'); $first->importo_pagato = $group->sum('importo_pagato'); } return $first; }) ->values(); }; $itemsAggregati = $aggregateItems($items); // Filtra righe in base alla vista scelta $displayItems = $itemsAggregati; if ($this->vista === 'unita' && $this->unitaId !== null) { $displayItems = $itemsAggregati->where('unita_immobiliare_id', (int) $this->unitaId)->values(); } // Pre-carica saldi iniziali (esercizi precedenti) da dett_tab importato (CONG.*) $saldoRowsAll = $this->loadSaldiInizialiRows((int) $stabileId, $items, $unitaAll); $saldoRowsDisplay = $saldoRowsAll; if ($this->vista === 'unita' && $this->unitaId !== null) { $saldoRowsDisplay = array_values(array_filter($saldoRowsAll, fn ($r) => (int) ($r['unita_id'] ?? 0) === (int) $this->unitaId)); } $this->totaliUnita['rate'] = $this->unitaId !== null ? (int) $displayItems->count() : 0; $this->totaliUnita['addebitato'] = $this->unitaId !== null ? (float) $displayItems->sum(fn (RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto) : 0.0; $this->totaliUnita['pagato'] = $this->unitaId !== null ? (float) $displayItems->sum(fn (RataEmessaNg $r) => (float) $r->importo_pagato) : 0.0; $this->totaliUnita['residuo'] = (float) ($this->totaliUnita['addebitato'] - $this->totaliUnita['pagato']); $rateRows = $displayItems ->map(function (RataEmessaNg $r): array { $u = $r->unitaImmobiliare; $pianoId = is_numeric($r->piano_rateizzazione_id ?? null) ? (int) $r->piano_rateizzazione_id : 0; $pianoLabel = (string) ($r->pianoRateizzazione?->descrizione ?? ''); $unitaShort = ''; if ($u instanceof UnitaImmobiliare) { $unitaShort = $this->formatUnitaShort($u); } $note = (string) ($r->note ?? ''); $nEmissione = null; if ($note !== '' && preg_match('/\bn_emissione=(\d+)\b/', $note, $m)) { $nEmissione = (int) ($m[1] ?? 0); } $dovuto = (float) ($r->importo_addebitato_soggetto ?? 0); $pagato = (float) ($r->importo_pagato ?? 0); $residuo = $dovuto - $pagato; return [ 'id' => (int) $r->id, 'piano_id' => $pianoId, 'piano' => $pianoLabel, 'data_emissione' => $r->data_emissione?->format('d/m/Y'), 'data_emissione_sort' => $r->data_emissione?->format('Y-m-d') ?? '9999-12-31', 'data_scadenza' => $r->data_scadenza?->format('d/m/Y'), 'avviso' => (int) ($r->numero_rata_progressivo ?? 0), 'n_emissione' => $nEmissione, 'descrizione' => (string) ($r->descrizione ?? ''), 'unita_short' => $unitaShort, 'dovuto' => $dovuto, 'data_pagamento' => $r->data_ultimo_pagamento?->format('d/m/Y'), 'pagato' => $pagato, 'residuo' => $residuo, ]; }) ->all(); // Ordina: saldi iniziali prima delle rate $this->rows = array_merge($saldoRowsDisplay, $rateRows); // Aggiorna totali includendo i saldi iniziali (solo dovuto/residuo) $saldoAll = array_sum(array_map(fn ($r) => (float) ($r['dovuto'] ?? 0), $saldoRowsAll)); $saldoDisplay = array_sum(array_map(fn ($r) => (float) ($r['dovuto'] ?? 0), $saldoRowsDisplay)); $this->totali['addebitato'] = (float) ($this->totali['addebitato'] + $saldoAll); $this->totali['residuo'] = (float) ($this->totali['addebitato'] - $this->totali['pagato']); $this->totaliUnita['addebitato'] = (float) ($this->totaliUnita['addebitato'] + $saldoDisplay); $this->totaliUnita['residuo'] = (float) ($this->totaliUnita['addebitato'] - $this->totaliUnita['pagato']); $this->groups = $this->buildGroups($this->rows); $this->hydrateIncassiForSoggetto((int) $stabileId, $items, $annoGestione ? (int) $annoGestione : null); } private function formatUnitaLabel(UnitaImmobiliare $u): string { $cod = (string) ($u->codice_unita ?? ('ID ' . $u->id)); $nome = trim((string) ($u->denominazione ?? '')); if ($nome === '' || preg_match('/^\s*area\s+condominiale\s*$/i', $nome)) { $nome = 'Unità'; } $pos = trim('Scala ' . ($u->scala ?? '-') . ' · Piano ' . ($u->piano ?? '-') . ' · Int. ' . ($u->interno ?? '-')); return trim($cod . ' — ' . $nome . ' (' . $pos . ')'); } private function formatUnitaShort(UnitaImmobiliare $u): string { $scala = trim((string) ($u->scala ?? '')); $piano = trim((string) ($u->piano ?? '')); $interno = trim((string) ($u->interno ?? '')); $parts = []; if ($scala !== '') { $parts[] = $scala; } if ($piano !== '') { $parts[] = 'P' . $piano; } if ($interno !== '') { $parts[] = 'Int.' . $interno; } return implode('-', $parts); } /** @param array> $rows */ private function buildGroups(array $rows): array { $rank = function (string $label): int { $l = mb_strtolower($label); if (str_contains($l, 'bilancio iniziale') || str_contains($l, 'situazione iniziale') || str_contains($l, 'esercizi precedenti')) { return -1; } if (str_contains($l, 'ordinaria')) { return 0; } if (str_contains($l, 'riscaldamento')) { return 1; } if (str_contains($l, 'straordinaria')) { return 2; } return 9; }; $by = []; foreach ($rows as $r) { $key = (string) (($r['piano_id'] ?? 0) ?: ($r['piano'] ?? '')); if (! array_key_exists($key, $by)) { $by[$key] = [ 'key' => $key, 'label' => (string) ($r['piano'] ?? 'Gestione'), 'rows' => [], 'min_emissione' => (string) ($r['data_emissione_sort'] ?? '9999-12-31'), 'rank' => $rank((string) ($r['piano'] ?? '')), 'totali' => ['dovuto' => 0.0, 'pagato' => 0.0, 'residuo' => 0.0], ]; } $by[$key]['rows'][] = $r; $by[$key]['totali']['dovuto'] += (float) ($r['dovuto'] ?? 0); $by[$key]['totali']['pagato'] += (float) ($r['pagato'] ?? 0); $by[$key]['totali']['residuo'] += (float) ($r['residuo'] ?? 0); $d = (string) ($r['data_emissione_sort'] ?? '9999-12-31'); if ($d < $by[$key]['min_emissione']) { $by[$key]['min_emissione'] = $d; } } $groups = array_values($by); usort($groups, function ($a, $b): int { $ra = (int) ($a['rank'] ?? 9); $rb = (int) ($b['rank'] ?? 9); if ($ra !== $rb) { return $ra <=> $rb; } return strcmp((string) $a['min_emissione'], (string) $b['min_emissione']); }); foreach ($groups as &$g) { usort($g['rows'], function ($a, $b): int { $da = (string) ($a['data_emissione_sort'] ?? '9999-12-31'); $db = (string) ($b['data_emissione_sort'] ?? '9999-12-31'); if ($da === $db) { return ((int) ($a['id'] ?? 0)) <=> ((int) ($b['id'] ?? 0)); } return strcmp($da, $db); }); unset($g['min_emissione']); unset($g['rank']); } return $groups; } /** * Costruisce righe "Bilancio iniziale / esercizi precedenti" per unità e ruolo (C/I) dal dominio. * Per evitare collisioni tra import multipli, usa la prima (min) legacy_year disponibile per ogni unità/ruolo. * * @param \Illuminate\Support\Collection $rateItems * @param array $unitaAll * @return array> */ private function loadSaldiInizialiRows(int $stabileId, $rateItems, array $unitaAll): array { if (! DbSchema::hasTable('dettaglio_importi_tabella') || ! DbSchema::hasTable('tabelle_millesimali')) { return []; } if (! DbSchema::hasColumn('tabelle_millesimali', 'stabile_id') || ! DbSchema::hasColumn('tabelle_millesimali', 'codice_tabella')) { return []; } $unitIds = array_values(array_unique(array_map(fn (UnitaImmobiliare $u) => (int) $u->id, $unitaAll))); if (empty($unitIds)) { return []; } // Inferisci tipo soggetto (C/I) per unità dalle note delle rate $tipoPerUnita = []; foreach ($rateItems as $r) { try { $uid = is_numeric($r->unita_immobiliare_id ?? null) ? (int) $r->unita_immobiliare_id : null; if (! $uid) { continue; } if (array_key_exists($uid, $tipoPerUnita)) { continue; } $note = (string) ($r->note ?? ''); if ($note !== '' && preg_match('/\btipo_soggetto=([CI])\b/', $note, $m)) { $tipoPerUnita[$uid] = (string) ($m[1] ?? ''); } } catch (\Throwable $e) { continue; } } $domainHasLegacyYear = DbSchema::hasColumn('dettaglio_importi_tabella', 'legacy_year'); $base = DB::table('dettaglio_importi_tabella as dit') ->join('tabelle_millesimali as tm', 'tm.id', '=', 'dit.tabella_millesimale_id') ->where('tm.stabile_id', $stabileId) ->whereIn('dit.unita_immobiliare_id', $unitIds) ->where('tm.codice_tabella', 'like', 'CONG.%') ->whereNotNull('dit.cons_euro'); // Se c'è legacy_year, seleziona solo la prima (min) per unità+ruolo per ottenere la "situazione iniziale" if ($domainHasLegacyYear) { $minYears = (clone $base) ->selectRaw('dit.unita_immobiliare_id, dit.ruolo_legacy, MIN(dit.legacy_year) as legacy_year') ->groupBy('dit.unita_immobiliare_id', 'dit.ruolo_legacy'); $base->joinSub($minYears, 'mn', function ($join) { $join->on('mn.unita_immobiliare_id', '=', 'dit.unita_immobiliare_id') ->on('mn.ruolo_legacy', '=', 'dit.ruolo_legacy') ->on('mn.legacy_year', '=', 'dit.legacy_year'); }); } $rows = $base ->selectRaw('dit.unita_immobiliare_id as unita_id, dit.ruolo_legacy as ruolo, tm.codice_tabella as codice_tabella, SUM(dit.cons_euro) as importo') ->groupBy('dit.unita_immobiliare_id', 'dit.ruolo_legacy', 'tm.codice_tabella') ->get(); if ($rows->isEmpty()) { return []; } $unitaById = []; foreach ($unitaAll as $u) { $unitaById[(int) $u->id] = $u; } $out = []; $seq = 0; foreach ($rows as $r) { $unitaId = is_numeric($r->unita_id ?? null) ? (int) $r->unita_id : 0; if (! $unitaId || ! array_key_exists($unitaId, $unitaById)) { continue; } $ruolo = strtoupper(trim((string) ($r->ruolo ?? ''))); $tipoAtteso = $tipoPerUnita[$unitaId] ?? null; if ($tipoAtteso && $ruolo !== $tipoAtteso) { continue; } $importo = is_numeric($r->importo ?? null) ? (float) $r->importo : 0.0; if (abs($importo) < 0.00001) { continue; } $u = $unitaById[$unitaId]; $unitaShort = $this->formatUnitaShort($u); $codTab = trim((string) ($r->codice_tabella ?? '')); $seq++; $out[] = [ 'id' => -1000000 - $seq, 'piano_id' => 0, 'piano' => 'Bilancio iniziale (esercizi precedenti)', 'data_emissione' => '', 'data_emissione_sort' => '1900-01-01', 'data_scadenza' => null, 'avviso' => 0, 'n_emissione' => null, 'descrizione' => 'Conguagli es. precedenti (' . $codTab . ')', 'unita_short' => $unitaShort, 'dovuto' => $importo, 'data_pagamento' => null, 'pagato' => 0.0, 'residuo' => $importo, 'unita_id' => $unitaId, 'ruolo' => $ruolo, ]; } return $out; } /** * Collega incassi al soggetto usando la stessa codifica d'unione delle rate: * - tipo_soggetto=([CI]) * - cod_cond_dest=... * * In incassi: cond_inquil + cod_cond_gescon (o cod_cond) */ private function hydrateIncassiForSoggetto(int $stabileId, $rateItems, ?int $annoGestione = null): void { $this->incassi = []; if (! DbSchema::hasTable('incassi')) { return; } $codCol = DbSchema::hasColumn('incassi', 'cod_cond_gescon') ? 'cod_cond_gescon' : (DbSchema::hasColumn('incassi', 'cod_cond') ? 'cod_cond' : null); $tipoCol = DbSchema::hasColumn('incassi', 'cond_inquil') ? 'cond_inquil' : (DbSchema::hasColumn('incassi', 'cond_inq') ? 'cond_inq' : null); if (! $codCol || ! $tipoCol) { return; } $pairs = []; foreach ($rateItems as $r) { try { $note = (string) ($r->note ?? ''); if ($note === '') { continue; } $tipo = ''; $cod = ''; if (preg_match('/\btipo_soggetto=([CI])\b/', $note, $mTipo)) { $tipo = (string) ($mTipo[1] ?? ''); } elseif (preg_match('/\bcond_inq=([CI])\b/', $note, $mTipoAlt)) { $tipo = (string) ($mTipoAlt[1] ?? ''); } if (preg_match('/\bcod_cond_dest=([^\s|]+)/', $note, $mCod)) { $cod = trim((string) ($mCod[1] ?? '')); } elseif (preg_match('/\bcod_cond_src=([^\s|]+)/', $note, $mCodAlt)) { $cod = trim((string) ($mCodAlt[1] ?? '')); } if ($tipo === '' || $cod === '') { continue; } $pairs[$tipo . '|' . $cod] = ['tipo' => $tipo, 'cod' => $cod]; } catch (\Throwable $e) { continue; } } if (empty($pairs)) { return; } $incassiQuery = Incasso::query(); // Per NetGescon NG, l'import incassi salva condominio_id = stabileId. if (DbSchema::hasColumn('incassi', 'condominio_id')) { $incassiQuery->where('condominio_id', $stabileId); } $incassiQuery->where(function ($q) use ($pairs, $codCol, $tipoCol): void { foreach ($pairs as $p) { $q->orWhere(function ($qq) use ($p, $codCol, $tipoCol): void { $qq->where($codCol, (string) $p['cod']) ->where($tipoCol, (string) $p['tipo']); }); } }); if ($annoGestione) { $annoCol = DbSchema::hasColumn('incassi', 'anno_rif') ? 'anno_rif' : (DbSchema::hasColumn('incassi', 'anno') ? 'anno' : null); $annoShort = (int) substr((string) $annoGestione, -2); $incassiQuery->where(function ($q) use ($annoCol, $annoGestione, $annoShort): void { if ($annoCol) { $q->where($annoCol, (string) $annoGestione) ->orWhere($annoCol, (string) $annoShort) ->orWhere($annoCol, str_pad((string) $annoShort, 2, '0', STR_PAD_LEFT)); } if (DbSchema::hasColumn('incassi', 'dt_empag')) { $q->orWhereYear('dt_empag', (int) $annoGestione); } if (DbSchema::hasColumn('incassi', 'data_pagamento')) { $q->orWhereYear('data_pagamento', (int) $annoGestione); } }); } $orderCol = DbSchema::hasColumn('incassi', 'dt_empag') ? 'dt_empag' : (DbSchema::hasColumn('incassi', 'data_pagamento') ? 'data_pagamento' : 'id'); $incassi = $incassiQuery ->orderByDesc($orderCol) ->orderByDesc('id') ->limit(2000) ->get(); $contoMovimentiBase = CasseBancheMovimenti::getUrl(panel: 'admin-filament'); $this->incassi = $incassi ->map(function (Incasso $i) use ($codCol, $tipoCol, $contoMovimentiBase): array { $codCond = (string) (data_get($i, $codCol) ?? ''); $tipo = (string) (data_get($i, $tipoCol) ?? ''); $importo = (float) ($i->importo_pagato_euro ?? $i->importo_pagato ?? 0); $dt = null; if (! empty($i->dt_empag)) { try { $dt = Carbon::parse($i->dt_empag); } catch (\Throwable $e) { $dt = null; } } $dp = null; if (! empty($i->data_pagamento)) { try { $dp = Carbon::parse($i->data_pagamento); } catch (\Throwable $e) { $dp = null; } } $annoRaw = $i->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) . ')'; } $contoId = is_numeric($i->conto_bancario_id ?? null) ? (int) $i->conto_bancario_id : null; $contoUrl = $contoId ? ($contoMovimentiBase . '?conto_id=' . $contoId) : null; $stato = trim((string) ($i->stato_riconciliazione ?? '')); $conf = is_numeric($i->confidenza_match ?? null) ? (float) $i->confidenza_match : null; $hasMov = ! empty($i->movimento_bancario_id); $semaforo = 'DA RICONCILIARE'; if ($hasMov || strtolower($stato) === 'riconciliato') { $semaforo = 'OK'; } elseif ($conf !== null && $conf >= 0.7) { $semaforo = 'MATCH'; } $codCassa = (string) ($i->cod_cassa_gescon ?? $i->cod_cassa ?? $i->cod_cassa_legacy ?? $i->cod_cassa_gescon ?? ''); return [ 'id' => (int) $i->id, 'tipo' => $tipo, 'cod_cond' => $codCond, 'anno' => $annoLabel, 'n_ricevuta' => $i->n_ricevuta ?? null, 'dt_empag' => $dt ? $dt->format('d/m/Y') : null, 'data_pagamento' => $dp ? $dp->format('d/m/Y') : null, 'importo' => $importo, 'descrizione' => (string) ($i->descrizione ?? ''), 'cod_cassa' => $codCassa, 'conto_id' => $contoId, 'conto_url' => $contoUrl, 'stato_riconciliazione' => $stato, 'confidenza_match' => $conf, 'semaforo' => $semaforo, ]; }) ->all(); } }