*/ 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 array $gestioneYearPolicy = []; 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); $this->headerNome = trim((string) ($this->soggetto->ragione_sociale ?: trim((string) $this->soggetto->nome . ' ' . (string) $this->soggetto->cognome))) ?: ('Soggetto #' . (int) $this->soggetto->id); $this->headerCodiceFiscale = $this->soggetto->codice_fiscale ?: null; $this->headerLegacyNome = null; $this->headerLegacyCodiceFiscale = null; if (! DbSchema::hasTable('rate_emesse')) { return; } $annoGestione = AnnoGestioneContext::resolveActiveAnno($user); $this->gestioneYearPolicy = GestioneVisibility::policyForStabile((int) $stabileId, $annoGestione ?: null); $visibleYears = (array) ($this->gestioneYearPolicy['visible_years'] ?? []); $countableYears = (array) ($this->gestioneYearPolicy['countable_years'] ?? []); if ($this->vista === 'unita' && $this->unitaId !== null && $this->hydrateLegacyUnitView((int) $stabileId, $visibleYears, $countableYears)) { 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); $items = $query->get() ->filter(fn(RataEmessaNg $r): bool => GestioneVisibility::matchesYear($this->resolveRateCalendarYear($r), $visibleYears)) ->values(); $itemsCountable = $items ->filter(fn(RataEmessaNg $r): bool => GestioneVisibility::matchesYear($this->resolveRateCalendarYear($r), $countableYears)) ->values(); // Totali soggetto (tutte le unità) $this->totali['rate'] = (int) $itemsCountable->count(); $this->totali['addebitato'] = (float) $itemsCountable->sum(fn(RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto); $this->totali['pagato'] = (float) $itemsCountable->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); } if ($this->vista === 'unita' && $this->unitaId !== null) { $reference = $this->resolveCurrentUnitReference($this->unitaId); if ($reference !== null) { $currentName = trim((string) ($reference['nome'] ?? '')); $currentCf = trim((string) ($reference['codice_fiscale'] ?? '')); if ($currentName !== '' && $currentName !== $this->headerNome) { $this->headerLegacyNome = $this->headerNome; $this->headerLegacyCodiceFiscale = $this->headerCodiceFiscale; $this->headerNome = $currentName; $this->headerCodiceFiscale = $currentCf !== '' ? $currentCf : 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(function ($q): void { $q->where('note', 'like', '%tipo_soggetto=C%') ->orWhere('note', 'like', '%cond_inq=C%') ->orWhere('note', 'like', '%o_r_s=C%'); }) ->distinct() ->pluck('soggetto_responsabile_id') ->map(fn($v) => (int) $v) ->values(); $inqIds = (clone $baseOther) ->where(function ($q): void { $q->where('note', 'like', '%tipo_soggetto=I%') ->orWhere('note', 'like', '%cond_inq=I%') ->orWhere('note', 'like', '%o_r_s=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); $itemsCountableAggregati = $aggregateItems($itemsCountable); // 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)); } $displayItemsCountable = $itemsCountableAggregati; if ($this->vista === 'unita' && $this->unitaId !== null) { $displayItemsCountable = $itemsCountableAggregati->where('unita_immobiliare_id', (int) $this->unitaId)->values(); } $this->totaliUnita['rate'] = $this->unitaId !== null ? (int) $displayItemsCountable->count() : 0; $this->totaliUnita['addebitato'] = $this->unitaId !== null ? (float) $displayItemsCountable->sum(fn(RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto) : 0.0; $this->totaliUnita['pagato'] = $this->unitaId !== null ? (float) $displayItemsCountable->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); } $gestioneLegacy = $this->extractGestioneLabelFromRateNote($note); $ricevutaLegacy = $this->extractNumeroRicevutaFromRateNote($note); $avvisoLegacy = $this->extractAvvisoCodeFromRateNote($note); $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), 'avviso_legacy' => $avvisoLegacy, 'n_emissione' => $nEmissione, 'descrizione' => (string) ($r->descrizione ?? ''), 'gestione_legacy' => $gestioneLegacy, 'numero_ricevuta' => $ricevutaLegacy, 'riferimento_legacy' => $this->buildLegacyReferenceLabel($gestioneLegacy, $ricevutaLegacy, $avvisoLegacy), '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) $this->groups = $this->buildGroups($this->rows); $this->hydrateIncassiForSoggetto((int) $stabileId, $items, $visibleYears); } /** * @param array $visibleYears * @param array $countableYears */ private function hydrateLegacyUnitView(int $stabileId, array $visibleYears = [], array $countableYears = []): bool { if ($this->unitaId === null) { return false; } $unita = UnitaImmobiliare::query() ->with('stabile:id,codice_stabile') ->where('stabile_id', $stabileId) ->find($this->unitaId); if (! $unita instanceof UnitaImmobiliare) { return false; } $legacyRateRows = $this->loadLegacyRateRowsForUnita($unita, $visibleYears); if ($legacyRateRows === []) { return false; } $legacyRow = $this->getLegacyCondominRowForUnita($unita); $ownerName = trim((string) ($legacyRow?->nom_cond ?? '')); $ownerCf = trim((string) ($legacyRow?->cond_cod_fisc ?? $legacyRow?->codice_fiscale ?? '')); $tenantName = trim((string) ($legacyRow?->inquil_nome ?? '')); $tenantCf = trim((string) ($legacyRow?->inquil_cod_fisc ?? '')); if ($ownerName !== '') { $this->headerLegacyNome = $this->headerNome; $this->headerLegacyCodiceFiscale = $this->headerCodiceFiscale; $this->headerNome = $ownerName; $this->headerCodiceFiscale = $ownerCf !== '' ? $ownerCf : null; } $this->unitaList = [[ 'id' => (int) $unita->id, 'label' => $this->formatUnitaLabel($unita), 'short' => $this->formatUnitaShort($unita), 'totale' => (float) collect($legacyRateRows)->sum(fn(array $row): float => (float) ($row['dovuto'] ?? 0)), 'residuo' => (float) collect($legacyRateRows)->sum(fn(array $row): float => (float) ($row['residuo'] ?? 0)), ]]; $this->prevUnitaId = null; $this->nextUnitaId = null; $this->recordCondominoId = $this->resolveLegacySoggettoId($ownerCf, $legacyRow?->id_cond ?? $legacyRow?->legacy_id_cond ?? null); $this->recordInquilinoId = $this->resolveLegacySoggettoId($tenantCf, null); $countableRows = collect($legacyRateRows) ->filter(fn(array $row): bool => GestioneVisibility::matchesYear(isset($row['year']) && is_numeric($row['year']) ? (int) $row['year'] : null, $countableYears)) ->values(); $this->totali = [ 'rate' => (int) $countableRows->count(), 'addebitato' => (float) $countableRows->sum(fn(array $row): float => (float) ($row['dovuto'] ?? 0)), 'pagato' => (float) $countableRows->sum(fn(array $row): float => (float) ($row['pagato'] ?? 0)), 'residuo' => (float) $countableRows->sum(fn(array $row): float => (float) ($row['residuo'] ?? 0)), ]; $this->totaliUnita = $this->totali; $this->rows = collect($legacyRateRows) ->map(function (array $row) use ($unita): array { $gestioneLegacy = trim((string) ($row['gestione'] ?? '')) ?: null; $ricevutaLegacy = is_numeric($row['numero_ricevuta'] ?? null) ? (int) $row['numero_ricevuta'] : null; $avvisoLegacy = trim((string) ($row['avviso_code'] ?? $row['avviso'] ?? '')) ?: null; $dovuto = (float) ($row['dovuto'] ?? 0); $pagato = (float) ($row['pagato'] ?? 0); return [ 'id' => (int) ($row['id'] ?? 0), 'piano_id' => 0, 'piano' => $gestioneLegacy !== null ? ('Gestione ' . $gestioneLegacy) : 'Gestione', 'data_emissione' => $row['data_emissione'] ?? null, 'data_emissione_sort' => $this->normalizeLegacyDateSort($row['data_emissione'] ?? null), 'data_scadenza' => $row['data_scadenza'] ?? null, 'avviso' => $ricevutaLegacy ?? $avvisoLegacy, 'avviso_legacy' => $avvisoLegacy, 'n_emissione' => $row['numero_emissione'] ?? null, 'descrizione' => (string) ($row['descrizione'] ?? ''), 'gestione_legacy' => $gestioneLegacy, 'numero_ricevuta' => $ricevutaLegacy, 'riferimento_legacy' => $this->buildLegacyReferenceLabel($gestioneLegacy, $ricevutaLegacy, $avvisoLegacy), 'unita_short' => $this->formatUnitaShort($unita), 'dovuto' => $dovuto, 'data_pagamento' => $row['data_pagamento'] ?? null, 'pagato' => $pagato, 'residuo' => (float) ($row['residuo'] ?? ($dovuto - $pagato)), ]; }) ->all(); $this->groups = $this->buildGroups($this->rows); $this->incassi = $this->loadLegacyIncassiForUnita($unita, $visibleYears); return true; } private function resolveRateCalendarYear(RataEmessaNg $rate): ?int { $year = GestioneVisibility::normalizeCalendarYear($rate->data_emissione?->format('Y')); if ($year !== null) { return $year; } $year = GestioneVisibility::normalizeCalendarYear($rate->data_scadenza?->format('Y')); if ($year !== null) { return $year; } $legacyYear = $this->extractLegacyYearFromRateNote((string) ($rate->note ?? '')); return GestioneVisibility::normalizeCalendarYear($legacyYear); } 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); } private function normalizeLegacyDateSort(?string $value): string { $raw = trim((string) ($value ?? '')); if ($raw === '') { return '9999-12-31'; } try { return Carbon::createFromFormat('d/m/Y', $raw)->format('Y-m-d'); } catch (\Throwable) { return '9999-12-31'; } } private function getLegacyCondominRowForUnita(UnitaImmobiliare $unita): ?object { if (! DbSchema::connection('gescon_import')->hasTable('condomin')) { return null; } $codStabile = trim((string) ($unita->stabile?->codice_stabile ?? '')); $interno = trim((string) ($unita->interno ?? '')); if ($codStabile === '' || $interno === '') { return null; } $query = DB::connection('gescon_import') ->table('condomin') ->where('cod_stabile', $codStabile) ->where('interno', $interno); $scala = trim((string) ($unita->scala ?? '')); if ($scala !== '') { $query->where('scala', $scala); } $desiredCols = [ 'legacy_id_cond', 'id_cond', 'cod_cond', 'nom_cond', 'codice_fiscale', 'cond_cod_fisc', 'inquil_nome', 'inquil_cod_fisc', ]; $actualCols = \Illuminate\Support\Facades\Schema::connection('gescon_import')->getColumnListing('condomin'); $selectCols = array_intersect($desiredCols, $actualCols); return $query ->orderByDesc('legacy_year') ->orderByDesc('id') ->first($selectCols); } /** @return array */ private function resolveLegacyCondIdsForUnita(UnitaImmobiliare $unita): array { $fallbackIds = []; $storedId = trim((string) ($unita->legacy_cond_id ?? '')); if ($storedId !== '') { $fallbackIds[] = $storedId; } if (! DbSchema::connection('gescon_import')->hasTable('condomin')) { return array_values(array_unique($fallbackIds)); } $codStabile = trim((string) ($unita->stabile?->codice_stabile ?? '')); $interno = trim((string) ($unita->interno ?? '')); if ($codStabile === '' || $interno === '') { return array_values(array_unique($fallbackIds)); } $query = DB::connection('gescon_import') ->table('condomin') ->where('cod_stabile', $codStabile) ->where('interno', $interno); $scala = trim((string) ($unita->scala ?? '')); if ($scala !== '') { $query->where('scala', $scala); } $resolved = $query ->orderByDesc('legacy_year') ->pluck('cod_cond') ->map(fn($value) => trim((string) $value)) ->filter(fn(string $value): bool => $value !== '') ->unique() ->values() ->all(); return collect($fallbackIds) ->concat($resolved) ->map(fn($value) => trim((string) $value)) ->filter(fn(string $value): bool => $value !== '') ->unique() ->values() ->all(); } /** @param array $candidateIds * @return array */ private function preferStoredLegacyCondIdForImportTable(UnitaImmobiliare $unita, string $table, string $column, array $candidateIds, string $codStabile): array { $storedId = trim((string) ($unita->legacy_cond_id ?? '')); if ($storedId === '' || $candidateIds === [] || ! DbSchema::connection('gescon_import')->hasTable($table)) { return $candidateIds; } $hasStoredRows = DB::connection('gescon_import') ->table($table) ->where('cod_stabile', $codStabile) ->where($column, $storedId) ->exists(); return $hasStoredRows ? [$storedId] : $candidateIds; } /** @param array $visibleYears * @return array> */ private function loadLegacyRateRowsForUnita(UnitaImmobiliare $unita, array $visibleYears = []): array { if (! DbSchema::connection('gescon_import')->hasTable('rate_emissioni_dettaglio')) { return []; } $legacyCondIds = $this->resolveLegacyCondIdsForUnita($unita); $codStabile = trim((string) ($unita->stabile?->codice_stabile ?? '')); if ($legacyCondIds === [] || $codStabile === '') { return []; } $legacyCondIds = $this->preferStoredLegacyCondIdForImportTable($unita, 'rate_emissioni_dettaglio', 'cod_cond', $legacyCondIds, $codStabile); $rows = DB::connection('gescon_import') ->table('rate_emissioni_dettaglio') ->where('cod_stabile', $codStabile) ->whereIn('cod_cond', $legacyCondIds) ->orderBy('data_emissione') ->orderBy('numero_emissione') ->orderBy('numero_ricevuta') ->orderBy('id') ->limit(5000) ->get(); $mapped = $rows->map(function ($row): array { $gestioneLabel = trim((string) ($row->anno_gestione ?? '')); $year = $this->extractEffectiveGestioneEndYearFromLabel($gestioneLabel); if ($year === null) { try { if (! empty($row->data_emissione)) { $year = GestioneVisibility::normalizeCalendarYear(Carbon::parse($row->data_emissione)->format('Y')); } } catch (\Throwable) { $year = null; } } $dovuto = is_numeric($row->importo_dovuto_euro ?? null) ? (float) $row->importo_dovuto_euro : (is_numeric($row->importo_dovuto ?? null) ? (float) $row->importo_dovuto : 0.0); $pagato = is_numeric($row->gia_pagato ?? null) ? (float) $row->gia_pagato : 0.0; $mapped = [ 'id' => (int) ($row->id ?? 0), 'numero_emissione' => is_numeric($row->numero_emissione ?? null) ? (int) $row->numero_emissione : null, 'data_emissione' => $this->formatLegacyDateValue($row->data_emissione ?? null), 'data_scadenza' => null, 'data_pagamento' => null, 'descrizione' => trim((string) ($row->descrizione ?? '')), 'avviso' => $this->buildLegacyAvvisoCodeFromRow($gestioneLabel !== '' ? $gestioneLabel : (string) ($row->anno_emissione ?? ''), is_numeric($row->numero_mese ?? null) ? (int) $row->numero_mese : null, $row->data_emissione ?? null), 'avviso_code' => $this->buildLegacyAvvisoCodeFromRow($gestioneLabel !== '' ? $gestioneLabel : (string) ($row->anno_emissione ?? ''), is_numeric($row->numero_mese ?? null) ? (int) $row->numero_mese : null, $row->data_emissione ?? null), 'numero_ricevuta' => is_numeric($row->numero_ricevuta ?? null) ? (int) $row->numero_ricevuta : null, 'gestione' => $gestioneLabel !== '' ? $gestioneLabel : null, 'tipo' => trim((string) ($row->cond_inq ?? $row->tipo_soggetto ?? '')) ?: null, 'dovuto' => $dovuto, 'pagato' => $pagato, 'residuo' => is_numeric($row->residuo_emesso ?? null) ? (float) $row->residuo_emesso : ($dovuto - $pagato), 'year' => $year, ]; return $mapped; }); if ($visibleYears !== []) { $mapped = $mapped->filter(fn(array $row): bool => GestioneVisibility::matchesYear(isset($row['year']) && is_numeric($row['year']) ? (int) $row['year'] : null, $visibleYears)); } return $mapped->values()->all(); } /** @param array $visibleYears * @return array> */ private function loadLegacyIncassiForUnita(UnitaImmobiliare $unita, array $visibleYears = []): array { if (! DbSchema::hasTable('incassi')) { return []; } $legacyCondIds = $this->resolveLegacyCondIdsForUnita($unita); if ($legacyCondIds === []) { 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) { return []; } $incassi = Incasso::query() ->when(DbSchema::hasColumn('incassi', 'condominio_id'), fn($query) => $query->where('condominio_id', $stabileId = (int) $unita->stabile_id)) ->whereIn($codCol, $legacyCondIds) ->orderByDesc(DbSchema::hasColumn('incassi', 'dt_empag') ? 'dt_empag' : (DbSchema::hasColumn('incassi', 'data_pagamento') ? 'data_pagamento' : 'id')) ->orderByDesc('id') ->limit(2000) ->get() ->filter(fn(Incasso $i): bool => GestioneVisibility::matchesYear($this->resolveIncassoCalendarYear($i), $visibleYears)) ->values(); $contoMovimentiBase = CasseBancheMovimenti::getUrl(panel: 'admin-filament'); return $incassi->map(function (Incasso $i) use ($codCol, $tipoCol, $contoMovimentiBase): array { $importo = (float) ($i->importo_pagato_euro ?? $i->importo_pagato ?? $i->importo_euro ?? $i->importo ?? 0); $contoId = is_numeric($i->conto_bancario_id ?? null) ? (int) $i->conto_bancario_id : null; return [ 'id' => (int) $i->id, 'tipo' => $tipoCol ? (string) (data_get($i, $tipoCol) ?? '') : '', 'cod_cond' => (string) (data_get($i, $codCol) ?? ''), 'anno' => (string) ($i->anno_rif ?? $i->anno ?? ''), 'n_ricevuta' => $i->n_ricevuta ?? $i->n_riferimento ?? null, 'dt_empag' => $this->formatLegacyDateValue($i->dt_empag ?? null), 'data_pagamento' => $this->formatLegacyDateValue($i->data_pagamento ?? null), 'importo' => $importo, 'descrizione' => (string) ($i->descrizione ?? ''), 'cod_cassa' => (string) ($i->cod_cassa_gescon ?? $i->cod_cassa ?? $i->cod_cassa_legacy ?? ''), 'conto_id' => $contoId, 'conto_url' => $contoId ? ($contoMovimentiBase . '?conto_id=' . $contoId) : null, 'stato_riconciliazione' => (string) ($i->stato_riconciliazione ?? ''), 'confidenza_match' => is_numeric($i->confidenza_match ?? null) ? (float) $i->confidenza_match : null, 'semaforo' => ! empty($i->movimento_bancario_id) ? 'OK' : 'DA RICONCILIARE', ]; })->all(); } private function resolveLegacySoggettoId(?string $codiceFiscale, mixed $legacyId = null): ?int { $cf = strtoupper(trim((string) ($codiceFiscale ?? ''))); if ($cf !== '') { $id = Soggetto::query()->whereRaw('UPPER(codice_fiscale) = ?', [$cf])->value('id'); if (is_numeric($id) && (int) $id > 0) { return (int) $id; } } if (is_numeric($legacyId) && (int) $legacyId > 0) { $id = Soggetto::query()->where('old_id', (int) $legacyId)->value('id'); if (is_numeric($id) && (int) $id > 0) { return (int) $id; } } return null; } private function extractEffectiveGestioneEndYearFromLabel(?string $gestioneLabel): ?int { $label = trim((string) ($gestioneLabel ?? '')); if ($label === '') { return null; } if (preg_match('/\b(19|20)\d{2}\/(\d{2})\b/', $label, $m)) { $startYear = (int) $m[1] . substr((string) $m[0], 2, 2); $prefix = (int) substr((string) $startYear, 0, 2); return ((int) ($prefix * 100)) + (int) ($m[2] ?? 0); } if (preg_match('/\b(19|20)\d{2}\b/', $label, $m)) { return (int) ($m[0] ?? 0); } return null; } private function buildLegacyAvvisoCodeFromRow(?string $gestioneLabel, ?int $numeroMese, mixed $dataEmissione): ?string { $month = $numeroMese; if ($month === null || $month <= 0) { return null; } try { if (! empty($dataEmissione)) { $year = Carbon::parse($dataEmissione)->format('Y'); return $year . str_pad((string) $month, 2, '0', STR_PAD_LEFT); } } catch (\Throwable) { } $year = $this->extractEffectiveGestioneEndYearFromLabel($gestioneLabel); return $year !== null ? ((string) $year . str_pad((string) $month, 2, '0', STR_PAD_LEFT)) : null; } private function formatLegacyDateValue(mixed $value): ?string { if (empty($value)) { return null; } try { return Carbon::parse($value)->format('d/m/Y'); } catch (\Throwable) { $raw = trim((string) $value); if ($raw === '') { return null; } if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $raw)) { return $raw; } } return null; } private function resolveCurrentUnitReference(int $unitaId): ?array { $relation = PersonaUnitaRelazione::query() ->with('persona') ->where('unita_id', $unitaId) ->where('attivo', true) ->orderByRaw("case when lower(tipo_relazione) = 'comproprietario' then 1 else 0 end") ->orderByRaw("case when riceve_comunicazioni = 1 then 0 else 1 end") ->orderByDesc('id') ->first(); $persona = $relation?->persona; if (! $persona) { return null; } $nome = trim((string) ($persona->ragione_sociale ?? '')); if ($nome === '') { $nome = trim((string) (($persona->cognome ?? '') . ' ' . ($persona->nome ?? ''))); } if ($nome === '') { $nome = trim((string) ($persona->nome ?? '')); } if ($nome === '') { return null; } return [ 'nome' => $nome, 'codice_fiscale' => trim((string) ($persona->codice_fiscale ?? '')) ?: null, 'tipo_relazione' => strtolower(trim((string) ($relation->tipo_relazione ?? ''))), 'ruolo_rate' => trim((string) ($relation->ruolo_rate ?? '')) ?: null, ]; } /** @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 = []; $emittedConguaglioKeys = []; foreach ($rateItems as $r) { try { $uid = is_numeric($r->unita_immobiliare_id ?? null) ? (int) $r->unita_immobiliare_id : null; if (! $uid) { continue; } $note = (string) ($r->note ?? ''); $tipo = $this->extractLegacyRoleFromRateNote($note); if (! array_key_exists($uid, $tipoPerUnita) && $tipo !== null) { $tipoPerUnita[$uid] = $tipo; } if ( $tipo !== null && str_starts_with(strtolower(trim((string) ($r->descrizione ?? ''))), 'conguaglio') ) { $legacyYear = $this->extractLegacyYearFromRateNote($note); $amount = round((float) ($r->importo_addebitato_soggetto ?? 0), 2); if ($legacyYear !== null) { $emittedConguaglioKeys[$uid . '|' . $this->makeConguaglioDedupKey($tipo, $legacyYear, $amount)] = true; } } } 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'); }); } $select = 'dit.unita_immobiliare_id as unita_id, dit.ruolo_legacy as ruolo, tm.codice_tabella as codice_tabella, SUM(dit.cons_euro) as importo'; if ($domainHasLegacyYear) { $select = 'dit.unita_immobiliare_id as unita_id, dit.ruolo_legacy as ruolo, dit.legacy_year as legacy_year, tm.codice_tabella as codice_tabella, SUM(dit.cons_euro) as importo'; } $rows = $base ->selectRaw($select) ->groupBy('dit.unita_immobiliare_id', 'dit.ruolo_legacy', 'tm.codice_tabella') ->when($domainHasLegacyYear, fn($query) => $query->groupBy('dit.legacy_year')) ->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; } if (isset($emittedConguaglioKeys[$unitaId . '|' . $this->makeConguaglioDedupKey($ruolo, (string) ($domainHasLegacyYear ? ($r->legacy_year ?? '') : ''), $importo)])) { 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; } private function extractLegacyRoleFromRateNote(string $note): ?string { $note = trim($note); if ($note === '') { return null; } foreach (['tipo_soggetto', 'cond_inq', 'o_r_s'] as $field) { if (preg_match('/\b' . preg_quote($field, '/') . '=([CI])\b/', $note, $m)) { return (string) ($m[1] ?? null); } } return null; } private function extractLegacyYearFromRateNote(string $note): ?string { if (preg_match('/\banno_emissione=([^|\s]+)/', $note, $m)) { $value = trim((string) ($m[1] ?? '')); return $value !== '' ? $value : null; } return null; } private function extractGestioneLabelFromRateNote(string $note): ?string { if (preg_match('/\banno_gestione=([^|\s]+)/', $note, $m)) { $value = trim((string) ($m[1] ?? '')); return $value !== '' ? $value : null; } return null; } private function extractNumeroRicevutaFromRateNote(string $note): ?int { if (preg_match('/\bnumero_ricevuta=(\d+)\b/', $note, $m)) { $value = (int) ($m[1] ?? 0); return $value > 0 ? $value : null; } return null; } private function extractAvvisoCodeFromRateNote(string $note): ?string { if (preg_match('/\bavviso_code=([^|\s]+)/', $note, $m)) { $value = trim((string) ($m[1] ?? '')); return $value !== '' ? $value : null; } return null; } private function buildLegacyReferenceLabel(?string $gestioneLabel, ?int $numeroRicevuta, ?string $avvisoCode): ?string { $parts = []; $gestioneLabel = trim((string) ($gestioneLabel ?? '')); if ($gestioneLabel !== '') { $parts[] = 'Gestione ' . $gestioneLabel; } if ($numeroRicevuta !== null && $numeroRicevuta > 0) { $parts[] = 'ricevuta n.' . $numeroRicevuta; } elseif ($avvisoCode !== null && trim($avvisoCode) !== '') { $parts[] = 'avviso ' . trim($avvisoCode); } return $parts !== [] ? implode(' · ', $parts) : null; } private function makeConguaglioDedupKey(string $role, string $legacyYear, float $amount): string { return strtoupper(trim($role)) . '|' . trim($legacyYear) . '|' . number_format(round($amount, 2), 2, '.', ''); } /** * 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, array $visibleYears = []): 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']); }); } }); $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() ->filter(fn(Incasso $i): bool => GestioneVisibility::matchesYear($this->resolveIncassoCalendarYear($i), $visibleYears)) ->values(); $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(); } private function resolveIncassoCalendarYear(Incasso $incasso): ?int { $year = GestioneVisibility::normalizeCalendarYear($incasso->anno_rif ?? null); if ($year !== null) { return $year; } $year = GestioneVisibility::normalizeCalendarYear($incasso->anno ?? null); if ($year !== null) { return $year; } try { if (! empty($incasso->dt_empag)) { return GestioneVisibility::normalizeCalendarYear(Carbon::parse($incasso->dt_empag)->format('Y')); } if (! empty($incasso->data_pagamento)) { return GestioneVisibility::normalizeCalendarYear(Carbon::parse($incasso->data_pagamento)->format('Y')); } } catch (\Throwable $e) { } return null; } }