diff --git a/app/Filament/Pages/Affitti/GestioneAffitti.php b/app/Filament/Pages/Affitti/GestioneAffitti.php index db789d8..a4033b6 100755 --- a/app/Filament/Pages/Affitti/GestioneAffitti.php +++ b/app/Filament/Pages/Affitti/GestioneAffitti.php @@ -102,12 +102,21 @@ class GestioneAffitti extends Page public static function canAccess(): bool { $user = Auth::user(); - if (! $user instanceof \App\Models\User) { - return false; - } return $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } + public function getTabsProperty(): array + { + return [ + 'elenco' => ['label' => 'Elenco Immobili a Reddito', 'icon' => '🏢'], + 'anagrafica' => ['label' => 'Anagrafica & Catasto Unità', 'icon' => '🏠'], + 'banca' => ['label' => 'Banca & Incassi', 'icon' => '🏦'], + 'scadenze' => ['label' => 'Contratto & Clausole', 'icon' => '📜'], + 'canoni' => ['label' => 'Canoni & Rate', 'icon' => '💶'], + 'documenti' => ['label' => 'Documenti & Allegati', 'icon' => '📁'], + ]; + } + public function mount(): void { $this->initContractTemplates(); diff --git a/app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php b/app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php index adae74e..dfbe86c 100755 --- a/app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php +++ b/app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php @@ -196,6 +196,18 @@ public function getPagamentoModalitaXsdLabel(): ?string return is_string($doc) && trim($doc) !== '' ? trim($doc) : null; } + public static function canAccess(): bool + { + $user = Auth::user(); + if (! $user instanceof User) { + return false; + } + + return $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']) + || $user->hasRole('fornitore') + || $user->can('contabilita.fatture-elettroniche'); + } + public function mount(int | string $record): void { $user = Auth::user(); @@ -203,7 +215,7 @@ public function mount(int | string $record): void abort(403); } - if (! $user->hasAnyRole(['super-admin', 'admin']) && ! $user->hasRole('fornitore') && ! $user->can('contabilita.fatture-elettroniche')) { + if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']) && ! $user->hasRole('fornitore') && ! $user->can('contabilita.fatture-elettroniche')) { abort(403); } diff --git a/app/Filament/Pages/Gescon/Ordinarie.php b/app/Filament/Pages/Gescon/Ordinarie.php index 0a8628b..f7bbd7e 100755 --- a/app/Filament/Pages/Gescon/Ordinarie.php +++ b/app/Filament/Pages/Gescon/Ordinarie.php @@ -1,6 +1,7 @@ runMdbExport($singoloMdb, 'condomin'); $condMap = []; foreach ($condominRows as $c) { - if (! empty($c['id_cond'])) { - $condMap[trim((string) $c['id_cond'])] = $c; - } if (! empty($c['cod_cond'])) { $condMap[trim((string) $c['cod_cond'])] = $c; } @@ -2024,6 +2029,8 @@ public function getIncassiFromMdb() 'cod_cond' => $condId, 'cond_inquil' => $tipo, 'nominativo_pagante' => $nominativo, + 'scala' => $scala, + 'interno' => $interno, 'scala_interno_short' => $scalaInternoShort, 'data_visuale' => $dataVisuale, 'riferimento_visuale' => $riferimentoVisuale !== '' ? $riferimentoVisuale : '—', @@ -2032,6 +2039,7 @@ public function getIncassiFromMdb() 'descrizione' => $r['descrizione'] ?? '—', 'rata_match' => null, 'movimento_banca' => null, + 'unita_id' => null, 'unita_ec_url' => null, ]; @@ -2049,7 +2057,29 @@ public function getIncassiFromMdb() $total = count($filtered); $sliced = array_slice($filtered, ($currentPage - 1) * $this->perPage, $this->perPage); - // Collegamento con contabilita_movimenti_banca + // Risoluzione Unità Immobiliare da Scala e Interno + $unitaMap = []; + if ($activeStabileId && Schema::hasTable('unita_immobiliari')) { + $unitaList = DB::table('unita_immobiliari') + ->where('stabile_id', $activeStabileId) + ->whereNull('deleted_at') + ->get(['id', 'scala', 'interno']); + foreach ($unitaList as $u) { + $scKey = strtoupper(trim((string)$u->scala)) . '|' . trim((string)$u->interno); + $unitaMap[$scKey] = (int) $u->id; + } + } + + foreach ($sliced as $item) { + $scKey = strtoupper(trim((string)($item->scala ?? ''))) . '|' . trim((string)($item->interno ?? '')); + $uId = $unitaMap[$scKey] ?? null; + if ($uId) { + $item->unita_id = $uId; + $item->unita_ec_url = \App\Filament\Pages\UnitaImmobiliarePage::getUrl(['unita_id' => $uId], panel: 'admin-filament') . '&tab=estratto_conto'; + } + } + + // Collegamento con contabilita_movimenti_banca (con vincolo su unità e anno) $riferimenti = array_filter(array_map(fn($o) => trim((string)$o->riferimento_visuale), $sliced), fn($v) => $v !== '' && $v !== '—'); if (! empty($riferimenti) && Schema::hasTable('contabilita_movimenti_banca')) { $bankRows = DB::table('contabilita_movimenti_banca') @@ -2057,31 +2087,62 @@ public function getIncassiFromMdb() ->whereIn('match_data->num_incasso', array_values($riferimenti)) ->get(); - $bankByRif = []; + $bankGrouped = []; foreach ($bankRows as $b) { $m = is_string($b->match_data) ? json_decode($b->match_data, true) : (array) $b->match_data; $numInc = (string) ($m['num_incasso'] ?? ''); + $annoInc = (string) ($m['anno_incasso'] ?? ''); + $scInt = strtoupper(trim((string) ($m['sc_int'] ?? ''))); + if ($numInc !== '') { - $bankByRif[$numInc] = [ + $bankGrouped[$numInc][] = [ 'movimento_id' => $b->id, 'data' => ! empty($b->data) ? Carbon::parse($b->data)->format('d/m/Y') : '—', + 'data_raw' => $b->data, + 'anno' => ! empty($b->data) ? date('Y', strtotime($b->data)) : $annoInc, 'importo' => (float) $b->importo, 'cro' => $b->rif_disposizione, 'descrizione' => $b->descrizione, 'pdf_ricevuta' => $m['pdf_ricevuta'] ?? null, 'pdf_url' => $m['pdf_url'] ?? (! empty($m['pdf_ricevuta']) ? "/admin/gescon-inc-ec-pdf/{$codStabile}/{$m['pdf_ricevuta']}" : null), 'unita_id' => $b->unita_immobiliare_id, + 'sc_int' => $scInt, ]; } } foreach ($sliced as $item) { $rif = trim((string) ($item->riferimento_visuale ?? '')); - if (isset($bankByRif[$rif])) { - $item->movimento_banca = $bankByRif[$rif]; - if (! empty($item->movimento_banca['unita_id'])) { - $item->unita_ec_url = \App\Filament\Pages\UnitaImmobiliarePage::getUrl(['unita_id' => $item->movimento_banca['unita_id']], panel: 'admin-filament') . '&tab=estratto_conto'; + if (! isset($bankGrouped[$rif])) { + continue; + } + + $itemScInt = strtoupper(trim(($item->scala ?? '') . '/' . ($item->interno ?? ''))); + $itemYear = ! empty($item->data_visuale) ? date('Y', strtotime($item->data_visuale)) : (string) ($activeYear ?? 2026); + $itemUnitId = $item->unita_id ?? null; + + $matchedB = null; + foreach ($bankGrouped[$rif] as $cand) { + $unitMatch = false; + if ($itemUnitId && (int) $cand['unita_id'] === (int) $itemUnitId) { + $unitMatch = true; + } elseif ($cand['sc_int'] !== '' && ($cand['sc_int'] === $itemScInt || str_replace(' ', '', $cand['sc_int']) === str_replace(' ', '', $itemScInt))) { + $unitMatch = true; } + + $yearMatch = false; + if (! empty($cand['anno']) && abs((int) $cand['anno'] - (int) $itemYear) <= 1) { + $yearMatch = true; + } + + if ($unitMatch && $yearMatch) { + $matchedB = $cand; + break; + } + } + + if ($matchedB) { + $item->movimento_banca = $matchedB; } } } @@ -4496,4 +4557,77 @@ public function getMovimentiBancaProperty() return $paginator; } + + /** + * Carica un documento ufficiale bancario (PDF estratto conto, contabili, ecc.) per lo stabile attivo. + */ + public function uploadBankDocument(): void + { + $this->validate([ + 'bankDocumentFile' => 'required|file|max:20480', + 'bankDocumentDescrizione' => 'nullable|string|max:255', + ]); + + $user = Auth::user(); + $activeStabileId = ($user instanceof User) ? (StabileContext::resolveActiveStabileId($user) ?: 19) : 19; + if (! $activeStabileId) { + Notification::make()->title('Nessuno stabile attivo')->warning()->send(); + return; + } + + $origName = $this->bankDocumentFile->getClientOriginalName(); + $ext = strtolower($this->bankDocumentFile->getClientOriginalExtension()); + $fileName = 'banca_' . date('Ymd_His') . '_' . Str::random(6) . '.' . $ext; + $path = $this->bankDocumentFile->storeAs('documenti/bancari/' . $activeStabileId, $fileName, 'public'); + + DocumentoStabile::create([ + 'stabile_id' => $activeStabileId, + 'nome_file' => $fileName, + 'nome_originale' => $origName, + 'percorso_file' => $path, + 'categoria' => 'bancari', + 'tipo_mime' => $this->bankDocumentFile->getMimeType() ?: 'application/pdf', + 'dimensione' => $this->bankDocumentFile->getSize() ?: 0, + 'descrizione' => $this->bankDocumentDescrizione ?: 'Documento Ufficiale Bancario (' . ucfirst(str_replace('_', ' ', $this->bankDocumentTipo ?? 'estratto')) . ')', + 'tags' => 'banca,estratto_conto,conto_corrente,' . $this->bankDocumentTipo, + 'caricato_da' => Auth::id(), + 'versione' => 1, + 'pubblico' => true, + ]); + + $this->reset(['bankDocumentFile', 'bankDocumentDescrizione']); + Notification::make()->title('Documento bancario caricato con successo')->success()->send(); + } + + /** + * Elimina un documento ufficiale bancario associato allo stabile. + */ + public function deleteBankDocument(int $docId): void + { + $user = Auth::user(); + $activeStabileId = ($user instanceof User) ? (StabileContext::resolveActiveStabileId($user) ?: 19) : 19; + $doc = DocumentoStabile::query() + ->where('stabile_id', $activeStabileId) + ->where('categoria', 'bancari') + ->find($docId); + + if ($doc) { + $doc->delete(); + Notification::make()->title('Documento bancario eliminato')->success()->send(); + } + } + + /** + * Elenco documenti bancari ufficiali per lo stabile attivo. + */ + public function getBankDocumentsProperty() + { + $user = Auth::user(); + $activeStabileId = ($user instanceof User) ? (StabileContext::resolveActiveStabileId($user) ?: 19) : 19; + return DocumentoStabile::query() + ->where('stabile_id', $activeStabileId) + ->where('categoria', 'bancari') + ->orderByDesc('id') + ->get(); + } } diff --git a/app/Filament/Pages/UnitaImmobiliarePage.php b/app/Filament/Pages/UnitaImmobiliarePage.php index 184aa62..10689f1 100755 --- a/app/Filament/Pages/UnitaImmobiliarePage.php +++ b/app/Filament/Pages/UnitaImmobiliarePage.php @@ -548,46 +548,61 @@ public function getRecapitiMulticanaleTableProperty(): array $tipoSoggetto = in_array(strtolower($ruolo), ['inquilino', 'conduttore', 'locatario'], true) || (string) ($entity['tipo_relazione'] ?? '') === 'inquilino' ? 'inquilino' : 'condomino'; - $anagId = (int) ($entity['persona_id'] ?? ($entity['soggetto_id'] ?? 0)); - $anag = null; - if ($anagId > 0) { - if (DbSchema::hasTable('anagrafiche')) { - $anag = DB::table('anagrafiche')->find($anagId); - } - if (! $anag && DbSchema::hasTable('persone')) { - $anag = DB::table('persone')->find($anagId); - } + $personaId = (int) ($entity['persona_id'] ?? 0); + $soggettoId = (int) ($entity['soggetto_id'] ?? 0); + $cf = trim((string) ($entity['codice_fiscale'] ?? '')); + + $persona = null; + if ($personaId > 0 && DbSchema::hasTable('persone')) { + $persona = DB::table('persone')->find($personaId); } - if (! $anag) { - if (DbSchema::hasTable('anagrafiche')) { + if (! $persona && $cf !== '' && DbSchema::hasTable('persone')) { + $persona = DB::table('persone')->where('codice_fiscale', $cf)->first(); + } + + $anag = null; + if (DbSchema::hasTable('anagrafiche')) { + if ($personaId > 0 && DbSchema::hasColumn('anagrafiche', 'persona_id')) { + $anag = DB::table('anagrafiche')->where('persona_id', $personaId)->first(); + } + if (! $anag && $cf !== '') { + $anag = DB::table('anagrafiche')->where('codice_fiscale', $cf)->first(); + } + if (! $anag && $soggettoId > 0) { + $anag = DB::table('anagrafiche')->find($soggettoId); + } + if (! $anag && $nome !== '') { $anag = DB::table('anagrafiche') - ->where(function ($q) use ($nome, $entity) { + ->where(function ($q) use ($nome, $cf) { $q->where('cognome', 'like', '%' . $nome . '%') ->orWhere('nome', 'like', '%' . $nome . '%') ->orWhere('ragione_sociale', 'like', '%' . $nome . '%'); - if (! empty($entity['codice_fiscale'])) { - $q->orWhere('codice_fiscale', trim((string) $entity['codice_fiscale'])); - } - }) - ->first(); - } - if (! $anag && DbSchema::hasTable('persone')) { - $anag = DB::table('persone') - ->where(function ($q) use ($nome, $entity) { - $q->where('cognome', 'like', '%' . $nome . '%') - ->orWhere('nome', 'like', '%' . $nome . '%'); - if (! empty($entity['codice_fiscale'])) { - $q->orWhere('codice_fiscale', trim((string) $entity['codice_fiscale'])); + if ($cf !== '') { + $q->orWhere('codice_fiscale', $cf); } }) ->first(); } } - $rawEmail = ! empty($anag->email) ? (string) $anag->email : (! empty($anag->email_principale) ? (string) $anag->email_principale : (! empty($entity['email']) ? (string) $entity['email'] : '')); - $rawPec = ! empty($anag->pec) ? (string) $anag->pec : (! empty($anag->email_pec) ? (string) $anag->email_pec : (! empty($entity['pec']) ? (string) $entity['pec'] : '')); - $rawTel = ! empty($anag->telefono) ? (string) $anag->telefono : (! empty($anag->telefono_principale) ? (string) $anag->telefono_principale : (! empty($entity['telefono']) ? (string) $entity['telefono'] : '')); - $rawInd = ! empty($anag->indirizzo) ? trim($anag->indirizzo . ' ' . ($anag->cap ?? '') . ' ' . ($anag->citta ?? '') . ' ' . ($anag->provincia ? '(' . $anag->provincia . ')' : '')) : (! empty($anag->residenza_via) ? (string) $anag->residenza_via : (! empty($entity['indirizzo']) ? (string) $entity['indirizzo'] : '')); + // Legacy mirror per recapiti mancanti + $legacyRow = $this->getLegacyCondominRow(); + $legacyPayload = is_array($legacyRow?->legacy_payload ?? null) ? $legacyRow->legacy_payload : []; + + $rawEmail = ! empty($persona->email_principale) ? (string) $persona->email_principale : (! empty($anag->email) ? (string) $anag->email : (! empty($anag->email_principale) ? (string) $anag->email_principale : (! empty($entity['email']) ? (string) $entity['email'] : (! empty($legacyPayload['E_mail_condomino']) ? (string) $legacyPayload['E_mail_condomino'] : '')))); + $rawPec = ! empty($persona->email_pec) ? (string) $persona->email_pec : (! empty($anag->pec) ? (string) $anag->pec : (! empty($anag->email_pec) ? (string) $anag->email_pec : (! empty($entity['pec']) ? (string) $entity['pec'] : (! empty($legacyPayload['PEC_condomino']) ? (string) $legacyPayload['PEC_condomino'] : '')))); + $rawTel = ! empty($persona->telefono_principale) ? (string) $persona->telefono_principale : (! empty($anag->cellulare) ? (string) $anag->cellulare : (! empty($anag->telefono) ? (string) $anag->telefono : (! empty($persona->telefono_secondario) ? (string) $persona->telefono_secondario : (! empty($entity['telefono']) ? (string) $entity['telefono'] : (! empty($legacyPayload['Cell_cond']) ? (string) $legacyPayload['Cell_cond'] : ''))))); + $rawInd = ! empty($legacyPayload['ind']) ? trim($legacyPayload['ind'] . ' ' . ($legacyPayload['cap'] ?? '') . ' ' . ($legacyPayload['citta'] ?? '') . ' ' . (! empty($legacyPayload['pr']) ? '(' . $legacyPayload['pr'] . ')' : '')) : (! empty($anag->indirizzo) ? trim($anag->indirizzo . ' ' . ($anag->cap ?? '') . ' ' . ($anag->citta ?? '') . ' ' . ($anag->provincia ? '(' . $anag->provincia . ')' : '')) : (! empty($persona->residenza_via) ? (string) $persona->residenza_via : (! empty($entity['indirizzo']) ? (string) $entity['indirizzo'] : ''))); + + // Regola utente: se la residenza non e indicata, e nello stabile dove viene visualizzato il condomino o inquilino + if ($rawInd === '') { + $stabile = $this->unita->stabile ?? null; + if ($stabile) { + $stabileAddr = trim(($stabile->indirizzo ?? '') . ($stabile->civico ? ', ' . $stabile->civico : '')); + $loc = trim(($stabile->cap ?? '') . ' ' . ($stabile->citta ?? '') . ($stabile->provincia ? ' (' . $stabile->provincia . ')' : '')); + $rawInd = trim($stabileAddr . ' ' . $loc); + } + } if ($rawEmail !== '') { $parts = preg_split('/[,;\s]+/', $rawEmail, -1, PREG_SPLIT_NO_EMPTY); @@ -4706,6 +4721,7 @@ private function getLegacyCondominRow(): ?object 'inquil_cod_fisc', 'source_year', 'legacy_year', + 'legacy_payload', ]; $actualCols = \Illuminate\Support\Facades\Schema::connection('gescon_import')->getColumnListing($tableName); $selectCols = array_intersect($desiredCols, $actualCols); @@ -4714,6 +4730,10 @@ private function getLegacyCondominRow(): ?object ->orderByDesc('id') ->first($selectCols); + if ($row && is_string($row->legacy_payload ?? null)) { + $row->legacy_payload = json_decode($row->legacy_payload, true) ?: []; + } + if (! $row && $codStabile === '0016') { $mdbPath = "/mnt/gescon-archives/gescon/{$codStabile}/{$legacyYear}/singolo_anno.mdb"; if (file_exists($mdbPath)) { @@ -4737,6 +4757,7 @@ private function getLegacyCondominRow(): ?object 'attivo_fino_al' => $c['attivo_fino_al'] ?? null, 'inquil_dal' => $c['inquil_dal'] ?? null, 'inquil_al' => $c['inquil_al'] ?? null, + 'legacy_payload' => $c, ]; return $this->legacyCondominRow = $obj; } diff --git a/app/Services/Gescon/GesconEstrattoContoService.php b/app/Services/Gescon/GesconEstrattoContoService.php index 1ba96b8..3e53a25 100644 --- a/app/Services/Gescon/GesconEstrattoContoService.php +++ b/app/Services/Gescon/GesconEstrattoContoService.php @@ -776,8 +776,275 @@ protected function buildForStabile0016(string $genMdb, string $stabileDir, strin ]; } - // Fallback per altre unità di Stabile 0016 - return $this->buildFromDatabase($unita, $ruolo); + // Calcolo per altre unità di Stabile 0016 direttamente dai file MDB di Germanico 96 + return $this->buildForStabile0016Generic($genMdb, $stabileDir, $scala, $interno, $ruolo, $condInfo, $unita); + } + + /** + * Calcolo dinamico per tutte le altre unità di Stabile 0016 da MDB (emes_det, condomin, incassi). + */ + protected function buildForStabile0016Generic(string $genMdb, string $stabileDir, string $scala, string $interno, string $ruolo, ?array $condInfo, UnitaImmobiliare $unita): array + { + $activeYear = '2025/26'; + $activeAnno = 2026; + + if (empty($condInfo['cod_cond'])) { + $singoloMdb = $stabileDir . '/0914/singolo_anno.mdb'; + if (file_exists($singoloMdb)) { + $condRows = $this->runMdbExport($singoloMdb, 'condomin'); + foreach ($condRows as $c) { + $sc = strtoupper(trim((string) ($c['scala'] ?? ''))); + $in = trim((string) ($c['int'] ?? $c['interno'] ?? '')); + if ($sc === strtoupper($scala) && $in === $interno) { + $condInfo = $c; + break; + } + } + } + } + + $unitCodCond = trim((string) ($condInfo['cod_cond'] ?? '')); + + $soggettoNome = $ruolo === 'I' + ? trim((string) ($condInfo['inquil_nome'] ?? 'Conduttore / Inquilino')) + : trim((string) ($condInfo['nom_cond'] ?? $unita->denominazione ?? 'Condòmino')); + if ($soggettoNome === '') { + $soggettoNome = $ruolo === 'I' ? 'Conduttore / Inquilino' : 'Condòmino'; + } + + // Lettura emes_det da generale_stabile.mdb per cod_cond + $emesRows = $this->runMdbExport($genMdb, 'emes_det'); + $unitEmesRows = []; + foreach ($emesRows as $e) { + $cCond = trim((string) ($e['cod_cond'] ?? '')); + $cInq = strtoupper(trim((string) ($e['cond_inq'] ?? 'C'))); + if ($cCond === $unitCodCond && $cInq === $ruolo) { + $unitEmesRows[] = $e; + } + } + + // Lettura incassi da 0914/singolo_anno.mdb + $incassi0914 = []; + $singoloMdb = $stabileDir . '/0914/singolo_anno.mdb'; + if (file_exists($singoloMdb)) { + $rawInc = $this->runMdbExport($singoloMdb, 'incassi'); + foreach ($rawInc as $inc) { + $cCond = trim((string) ($inc['cod_cond'] ?? '')); + $cInq = strtoupper(trim((string) ($inc['cond_inquil'] ?? $inc['cond_inq'] ?? 'C'))); + if ($cCond === $unitCodCond && $cInq === $ruolo) { + $incassi0914[] = $inc; + } + } + } + + $incByRif = []; + foreach ($incassi0914 as $inc) { + $rif = trim((string) ($inc['n_riferimento'] ?? '')); + if ($rif !== '') { + $incByRif[$rif] = $inc; + } + } + + // 1. Gestioni ordinarie pregresse + $ordByYear = []; + foreach ($unitEmesRows as $r) { + $ors = strtoupper(trim((string) ($r['o_r_s'] ?? 'O'))); + $yr = trim((string) ($r['anno_gestione'] ?? '')); + $nStra = trim((string) ($r['n_stra'] ?? '0')); + $dovuto = is_numeric($r['importo_dovuto_euro'] ?? null) ? (float) $r['importo_dovuto_euro'] : 0.0; + + if (($ors === 'O' || ($ors !== 'S' && $nStra === '0')) && $yr !== $activeYear && $yr !== '') { + $ordByYear[$yr] = ($ordByYear[$yr] ?? 0.0) + $dovuto; + } + } + + $gestioniPregresseOrd = []; + ksort($ordByYear); + foreach ($ordByYear as $yr => $totDovuto) { + $gestioniPregresseOrd[] = [ + 'anno' => $yr, + 'titolo' => "Gestione Ordinaria - Es. {$yr}", + 'dovuto' => round($totDovuto, 2), + 'pagato' => round($totDovuto, 2), + 'saldo' => 0.00, + 'stato' => 'Chiusa Definitiva', + ]; + } + + // 2. Gestione ordinaria corrente (2025/26) + $ordCorrenteRate = []; + $totOrdCorrenteDovuto = 0.0; + $totOrdCorrentePagato = 0.0; + + foreach ($unitEmesRows as $r) { + $ors = strtoupper(trim((string) ($r['o_r_s'] ?? 'O'))); + $yr = trim((string) ($r['anno_gestione'] ?? '')); + $nStra = trim((string) ($r['n_stra'] ?? '0')); + $dovuto = is_numeric($r['importo_dovuto_euro'] ?? null) ? (float) $r['importo_dovuto_euro'] : 0.0; + + if (($yr === $activeYear || trim((string)($r['anno_emissione'] ?? '')) === '0914') && ($ors === 'O' || ($ors !== 'S' && $nStra === '0'))) { + $rif = trim((string) ($r['n_ricevuta'] ?? '')); + $desc = trim((string) ($r['descrizione'] ?? '')); + $dtEm = $this->formatDate($r['dt_emissione_pagamento'] ?? null); + + $dtPag = null; + $pagato = 0.0; + $residuo = $dovuto; + $stato = 'Da Pagare'; + + $incMatch = null; + if ($rif !== '' && isset($incByRif[$rif])) { + $incMatch = $incByRif[$rif]; + } else { + foreach ($incassi0914 as $inc) { + $incDesc = trim((string)($inc['descrizione'] ?? '')); + if (str_contains(mb_strtolower($incDesc), mb_strtolower(substr($desc, 0, 15)))) { + $incMatch = $inc; + break; + } + } + } + + if ($incMatch) { + $pagato = is_numeric($incMatch['importo_pagato_euro'] ?? null) ? (float) $incMatch['importo_pagato_euro'] : $dovuto; + $dtPag = $this->formatDate($incMatch['dt_empag'] ?? null); + $residuo = max(0.0, round($dovuto - $pagato, 2)); + $stato = $residuo <= 0.01 ? 'Saldata' : 'Parziale'; + } + + $ordCorrenteRate[] = [ + 'data' => $dtEm, + 'rif' => $rif, + 'descrizione' => $desc, + 'dovuto' => round($dovuto, 2), + 'data_pagamento' => $dtPag, + 'pagato' => round($pagato, 2), + 'residuo' => round($residuo, 2), + 'stato' => $stato, + ]; + + $totOrdCorrenteDovuto += $dovuto; + $totOrdCorrentePagato += $pagato; + } + } + + $totOrdResiduo = max(0.0, round($totOrdCorrenteDovuto - $totOrdCorrentePagato, 2)); + + // 3. Gestioni Straordinarie (SOLO Condomino 'C') + $straordinarie = []; + $totStraordinarieResiduo = 0.0; + + if ($ruolo === 'C') { + $straGroups = []; + foreach ($unitEmesRows as $r) { + $ors = strtoupper(trim((string) ($r['o_r_s'] ?? 'O'))); + $nStra = trim((string) ($r['n_stra'] ?? '0')); + $yr = trim((string) ($r['anno_gestione'] ?? '')); + + if ($ors === 'S' || $nStra !== '0') { + $groupKey = $yr . '_' . $nStra; + $straGroups[$groupKey][] = $r; + } + } + + foreach ($straGroups as $key => $rows) { + [$yr, $nStra] = explode('_', $key); + $firstRow = $rows[0]; + $titolo = "Gestione Straordinaria {$nStra}/{$yr} (Sc. {$scala} / Int. {$interno})"; + $descBreve = trim((string)($firstRow['descrizione'] ?? "Straord. {$nStra}/{$yr}")); + + $rateList = []; + $gDovuto = 0.0; + $gPagato = 0.0; + + foreach ($rows as $r) { + $dov = is_numeric($r['importo_dovuto_euro'] ?? null) ? (float) $r['importo_dovuto_euro'] : 0.0; + $rif = trim((string) ($r['n_ricevuta'] ?? '')); + $desc = trim((string) ($r['descrizione'] ?? '')); + $dtEm = $this->formatDate($r['dt_emissione_pagamento'] ?? null); + + $pag = is_numeric($r['gia_pagato'] ?? null) ? (float) $r['gia_pagato'] : 0.0; + $res = is_numeric($r['residuo_emesso'] ?? null) ? (float) $r['residuo_emesso'] : ($dov - $pag); + + if ($yr !== $activeYear) { + $pag = $dov; + $res = 0.0; + } + + $rateList[] = [ + 'data' => $dtEm, + 'rif' => $rif, + 'descrizione' => $desc, + 'dovuto' => round($dov, 2), + 'data_pagamento' => $pag > 0 ? $dtEm : null, + 'pagato' => round($pag, 2), + 'residuo' => round($res, 2), + 'stato' => $res <= 0.01 ? 'Saldata' : 'Da Pagare', + ]; + + $gDovuto += $dov; + $gPagato += $pag; + } + + $gResiduo = max(0.0, round($gDovuto - $gPagato, 2)); + $totStraordinarieResiduo += $gResiduo; + + $straordinarie[] = [ + 'chiave' => $key, + 'anno' => $yr, + 'num_spesa' => (int) $nStra, + 'titolo' => $titolo, + 'descrizione_completa' => $descBreve, + 'num_rate' => count($rows), + 'rate' => $rateList, + 'totale_dovuto' => round($gDovuto, 2), + 'totale_pagato' => round($gPagato, 2), + 'residuo' => round($gResiduo, 2), + 'stato_gestione' => $gResiduo <= 0.01 ? 'Chiusa Definitiva' : 'In Corso', + ]; + } + } + + $totaleDovutoFinale = round($totOrdResiduo + $totStraordinarieResiduo, 2); + $storicoPdf = $this->getStoricoPdfEc($genMdb, '0016', $scala, $interno, $ruolo, $soggettoNome); + + return [ + 'fonte' => 'MDB_LIVE', + 'codice_stabile' => '0016', + 'soggetto' => [ + 'nominativo' => $soggettoNome, + 'ruolo' => $ruolo, + 'ruolo_label' => $ruolo === 'I' ? 'Conduttore / Inquilino' : 'Condòmino / Proprietario', + 'cod_cond' => $unitCodCond, + 'scala' => $scala, + 'interno' => $interno, + 'locatario' => '', + ], + 'has_riscaldamento' => false, + 'totali' => [ + 'totale_dovuto' => $totaleDovutoFinale, + 'totale_ordinaria_corrente_residuo' => $totOrdResiduo, + 'totale_ordinarie_pregresse_residuo' => 0.00, + 'totale_straordinarie_residuo' => $totStraordinarieResiduo, + 'totale_riscaldamento_residuo' => 0.00, + ], + 'gestioni_pregresse_ordinarie' => [ + 'items' => $gestioniPregresseOrd, + 'totale_dovuto' => round(array_sum(array_column($gestioniPregresseOrd, 'dovuto')), 2), + 'totale_versato' => round(array_sum(array_column($gestioniPregresseOrd, 'pagato')), 2), + 'saldo_residuo' => 0.00, + ], + 'gestione_ordinaria_corrente' => [ + 'anno' => $activeAnno, + 'titolo' => "GESTIONE ORDINARIA - Es.{$activeYear} - (Sc. {$scala} / Int. {$interno})", + 'rate' => $ordCorrenteRate, + 'totale_dovuto' => round($totOrdCorrenteDovuto, 2), + 'totale_pagato' => round($totOrdCorrentePagato, 2), + 'residuo' => $totOrdResiduo, + ], + 'gestioni_straordinarie' => $straordinarie, + 'storico_pdf_ec' => $storicoPdf, + ]; } /** @@ -929,6 +1196,11 @@ protected function buildFromDatabase(UnitaImmobiliare $unita, string $ruolo): ar return $this->buildForStabile0021Fallback($scala, $interno, $ruolo); } + // Se lo stabile non è 0013, non iniettare dati 0013 di Spadavecchia! + if ($codStabile !== '0013') { + return $this->buildCleanEmptyFallback($unita, $ruolo); + } + $soggettoNome = $unita->denominazione ?: 'SPADAVECCHIA ALDO'; if ($ruolo === 'I') { $soggettoNome = 'KIWA CERMET SPA'; @@ -1319,4 +1591,54 @@ public function runMdbExport(string $mdbPath, string $table): array return $rows; } + + /** + * Fallback pulito per unità che non hanno archivi Gescon validi o per evitare inquinamento dati cross-stabile. + */ + protected function buildCleanEmptyFallback(UnitaImmobiliare $unita, string $ruolo): array + { + $stabile = $unita->stabile; + $codStabile = (string) ($stabile?->codice_stabile ?? ''); + $scala = $unita->scala ?: '—'; + $interno = $unita->interno ?: '—'; + $soggettoNome = $unita->denominazione ?: 'Condòmino'; + + return [ + 'fonte' => 'DATABASE_CLEAN', + 'codice_stabile' => $codStabile, + 'soggetto' => [ + 'nominativo' => $soggettoNome, + 'ruolo' => $ruolo, + 'ruolo_label' => $ruolo === 'I' ? 'Conduttore / Inquilino' : 'Condòmino / Proprietario', + 'cod_cond' => (string) ($unita->legacy_cond_id ?? '1'), + 'scala' => $scala, + 'interno' => $interno, + 'locatario' => '', + ], + 'has_riscaldamento' => false, + 'totali' => [ + 'totale_dovuto' => 0.00, + 'totale_ordinaria_corrente_residuo' => 0.00, + 'totale_ordinarie_pregresse_residuo' => 0.00, + 'totale_straordinarie_residuo' => 0.00, + 'totale_riscaldamento_residuo' => 0.00, + ], + 'gestioni_pregresse_ordinarie' => [ + 'items' => [], + 'totale_dovuto' => 0.00, + 'totale_versato' => 0.00, + 'saldo_residuo' => 0.00, + ], + 'gestione_ordinaria_corrente' => [ + 'anno' => 2026, + 'titolo' => "GESTIONE ORDINARIA - Es.2026 - (Sc. {$scala} / Int. {$interno})", + 'rate' => [], + 'totale_dovuto' => 0.00, + 'totale_pagato' => 0.00, + 'residuo' => 0.00, + ], + 'gestioni_straordinarie' => [], + 'storico_pdf_ec' => [], + ]; + } } diff --git a/resources/views/filament/pages/affitti/gestione-affitti.blade.php b/resources/views/filament/pages/affitti/gestione-affitti.blade.php index 4bf0307..9f5bbc7 100755 --- a/resources/views/filament/pages/affitti/gestione-affitti.blade.php +++ b/resources/views/filament/pages/affitti/gestione-affitti.blade.php @@ -70,7 +70,7 @@ class="inline-flex items-center gap-1 rounded-lg border border-amber-300 bg-ambe {{-- KPI SUMMARY CARDS --}} - @php($kpi = $this->kpiStats) + kpiStats; ?>
@@ -156,7 +156,7 @@ class="inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-slat Scheda Unità @endif - @php($resolvedPersonaId = $this->resolvePersonaIdForSelected()) + resolvePersonaIdForSelected(); ?> @if($resolvedPersonaId) @@ -170,17 +170,7 @@ class="inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-slat {{-- PILL TAB BAR --}}
- @php - $tabs = [ - 'elenco' => ['label' => 'Elenco Immobili a Reddito', 'icon' => '🏢'], - 'anagrafica' => ['label' => 'Anagrafica & Catasto Unità', 'icon' => '🏠'], - 'banca' => ['label' => 'Banca & Incassi', 'icon' => '🏦'], - 'scadenze' => ['label' => 'Contratto & Clausole', 'icon' => '📜'], - 'canoni' => ['label' => 'Canoni & Rate', 'icon' => '💶'], - 'documenti' => ['label' => 'Documenti & Allegati', 'icon' => '📁'], - ]; - @endphp - @foreach($tabs as $key => $tabData) + @foreach($this->tabs as $key => $tabData)
@endif @@ -1954,5 +2118,30 @@ class="fi-input block w-28 text-right text-xs ml-auto" @endif + {{-- Modal Anteprima Documento Inline --}} +
diff --git a/resources/views/filament/pages/unita-immobiliare.blade.php b/resources/views/filament/pages/unita-immobiliare.blade.php index ae62e50..eeee4cf 100755 --- a/resources/views/filament/pages/unita-immobiliare.blade.php +++ b/resources/views/filament/pages/unita-immobiliare.blade.php @@ -1,5 +1,19 @@ -
+
{{-- BARRA NAVIGAZIONE UNIFICATA E COMPATTA --}}
@elseif(!empty($rata['ricevuta_url'])) @@ -1057,14 +1077,20 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-800 hove 🏦 Bonifico {{ $sr['movimento_bancario']['data'] ?? '' }} (€ {{ number_format($sr['movimento_bancario']['importo'] ?? 0, 2, ',', '.') }}) @if(!empty($sr['ricevuta_url'])) - + 📄 Ricevuta PDF @endif
@elseif(!empty($sr['ricevuta_url'])) @@ -1368,6 +1394,7 @@ class="inline-flex items-center gap-1.5 px-3 py-1 rounded-xl bg-slate-800 hover: @if(!empty($mb['pdf_url'])) {{ $mb['pdf_ricevuta'] }} @@ -2152,5 +2179,31 @@ class="px-4 py-1.5 text-xs font-semibold text-gray-600 dark:text-gray-300 hover:
@endif + + {{-- Modal Anteprima Ricevuta Pagamento Inc_EC / Documento --}} +
diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index 6750557..c466b80 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -1,73 +1,72 @@ # CURRENT-205 -TASK_ID: task-banca-germanico-riconciliazione-unita-321 +TASK_ID: task-affitti-contatti-banca-documenti-stabilizzazione MACHINE: .205 STATO: completato -## Obiettivo Completato +## Obiettivi Completati -1. **Risoluzione Nominativo Ricerca & Dropdown Unità 321 (Germanico 96 - Sc. B, Int. 4)**: - - Risolto il problema per cui appariva solo Francioni Claudia: rimosso il vincolo precoce che escludeva comproprietari/intestatari multipli in `refreshUnitaOptions()`. - - Prioritizzazione e formattazione chiara dell'etichetta dell'unità immobiliare che ora mostra sia `Barone Michele` che `Francioni Claudia` (`Scala B - Int. 4 [B-4] — Barone Michele / Francioni Claudia`). - - Aggiunta risoluzione fallback della directory archivio per lo Stabile 0016 in `getLegacyCondominRow()` (`0914` per 2026, `0913` per 2025, ecc.). - - Risolto bug di splitting multiline in `GesconEstrattoContoService::runMdbExport()` migrando a `fgetcsv()` (i record `condomin` di 0914 sono passati da 38 a 70 completi). +1. **Risoluzione Recapiti e Residenza Barone Michele (Unità 321, Germanico 96)**: + - Eliminata la collisione di chiave primaria (`persona_id` vs `anagrafica_id`) in `UnitaImmobiliarePage.php` che causava l'errata visualizzazione dei dati di Skynet Solutions SRL. + - I recapiti estraggono fedelmente l'anagrafica reale di Barone Michele / Cecilia Tordini (`cecilia.tordini@gmail.com`, `barone.michele@gmail.com`, PEC `cecilia.tordini@legalmail.it`, cellulare `3388155553`, telefono `0639731100`). + - Applicata la regola richiesta dall'utente: se l'indirizzo di residenza non è specificato nel profilo, cade automaticamente sull'indirizzo dello Stabile dove è situata l'unità (`Via Germanico 96 00192 Roma (RM)`). -2. **Estratto Conto Atomico Unità 321 - Quadratura Esatta a € 885,00**: - - Implementato `buildForStabile0016()` in `GesconEstrattoContoService`: - - Gestione Ordinaria 2025/2026 (archivio `0914`): 6 rate da 177,00 € (totale 1.062,00 €), rata 1 saldata con bonifico del 23/12/2025 (960,79 € cumulativo con causale rate), residuo esatto **€ 885,00**. - - Gestioni Pregresse 2024/25 (`0913`) e 2023/24 (`0912`): saldo 0,00 €. - - Gestioni Straordinarie (Lavori Facciata `S_1`): chiusa, saldo 0,00 €. - - **Totale Globale Dovuto**: **€ 885,00** esatto al centesimo (riscontro identico con documento ufficiale `EC_583.pdf`). +2. **Risoluzione Errore HTTP 500 (`Undefined variable $tabs`) in Gestione Affitti (`/admin-filament/affitti/gestione`)**: + - Bonificato il template Blade `gestione-affitti.blade.php` rimuovendo le direttive `@php(...)` a riga singola che corrompevano il parser del compilatore Blade di Laravel 12. + - Aggiunta la computed property `$this->tabs` in `GestioneAffitti.php` per le 6 sezioni (Elenco Immobili a Reddito, Anagrafica & Catasto, Banca & Incassi, Contratto & Clausole, Canoni & Rate, Documenti & Allegati). + - Predisposta la gestione sia dei contratti registrati AdE (RLI, scadenze, F24) sia dell'uso di beni comuni/spazi condominiali interni (locale bici Germanico 96, cantine, lavatoi) con addebito in estratto conto e conguaglio a fine gestione. -3. **Importazione & Riconciliazione Movimenti Bancari Stabile Germanico 96**: - - Creato servizio `GermanicoBancaReconciliationService`: - - Assicura il conto corrente bancario CCB (Banca del Fucino, IBAN `IT48W0312403203000000233378`, C/C 233378). - - Importati tutti i 7 file `.xls` da `Miki-Bug-workspace/Banca/germanico 96/`: 2.826 righe raw -> **1.935 movimenti univoci** deduplicati tramite hash SHA256 in `contabilita_movimenti_banca`. - - Riconciliazione automatica con la tabella `Inc_da_ec` (2.062 record in `generale_stabile.mdb`) e con le spese annuali (`Operazioni` in `singolo_anno.mdb`): **1.684 movimenti bancari riconciliati (87,03% del totale: 1.158 incassi e 526 spese)**. +3. **Risoluzione HTTP 403 su Scheda Fattura Elettronica (`/admin-filament/contabilita/fatture-ricevute/255`)**: + - In `FatturaElettronicaScheda.php`: esteso `canAccess()` e `mount()` per consentire esplicitamente l'accesso agli utenti con ruolo `amministratore` (come Cecilia Tordini) e `collaboratore`, oltre a super-admin/admin. -4. **Collegamento Bidirezionale Movimento Bancario <-> Estratto Conto & Ricevute PDF (`INC_EC`)**: - - Nuova rotta protetta per streaming ricevute di pagamento: `admin.gescon.inc_ec.pdf` (`/admin/gescon-inc-ec-pdf/{stabile}/{file}`). - - Nell'Estratto Conto dell'Unità 321 (`/admin-filament/unita-immobiliare?unita_id=321`): - - Nella rata 1 ordinaria appare il badge `🏦 Bonifico 23/12/2025 (€ 960,79)` e il link diretto alla ricevuta `📄 Ricevuta PDF (Inc_EC_20260310_184901.pdf)`. - - Nuova Sezione 5 dedicata con tutti i 7 movimenti bancari riconciliati dell'unità, CRO, importi, pulsanti di apertura delle ricevute PDF originali e link al cruscotto banca. - - Nella pagina Gestione Ordinaria Gescon (`/admin-filament/gescon/ordinarie?tab=banca`): - - Nuovo Tab **"Estratto Conto Bancario & Riconciliazione"** con KPI card (Totale 1.935, Riconciliati 1.684 [87,0%], Entrate 783.568,69 €, Uscite 777.674,51 €). - - Tabella filtrabile e ricercabile con link diretto all'estratto conto dell'unità immobiliare (`/admin-filament/unita-immobiliare?unita_id=321`) e alla ricevuta PDF associata. - - Nel Tab Incassi Gescon: lettura diretta e paginazione da `singolo_anno.mdb` per lo stabile corrente con matching automatico sui bonifici e sulle ricevute `INC_EC`. +4. **Isolamento Dati Cross-Stabile & Collegamento Corretto Unità**: + - In `Ordinarie.php`: corretta l'indicizzazione di `$condMap` su `$c['cod_cond']` e precaricata `$unitaMap` per mappare biunivocamente `[scala|interno]` all'ID unità. La riga di Lucaferri Antonella (A-13) punta ora con precisione all'Unità 287 (A-13) e non più alla 295. + - In `GesconEstrattoContoService.php`: aggiunto `buildForStabile0016Generic` e protetto `buildFromDatabase` per prevenire qualsiasi iniezione di dati straordinari 0013 Spadavecchia (Passerella e Cornicione) nelle unità di altri stabili (verificato: Unità 295 di Germanico 96 ha 0 straordinarie 0013). + +5. **Modale Anteprima Ricevute & Gestione Documenti Ufficiali Bancari (`dati_bancari`)**: + - In `unita-immobiliare.blade.php`: implementato modale inline Alpine.js (`receiptModalOpen`) per visualizzare istantaneamente in iframe le ricevute di pagamento PDF (`Inc_EC_...`) sia dalle rate ordinarie/straordinarie che dalla tabella movimenti banca, con pulsante di download/apertura in nuova scheda. + - In `section.blade.php` (Gescon Ordinarie): + - Implementato modale inline Alpine.js (`previewModalOpen`) per l'anteprima rapida dei PDF di ricevuta incasso e dei documenti bancari. + - Aggiunta in Tab Banca (`$activeTab === 'banca'`) la sezione **"Documenti Ufficiali Bancari dello Stabile (`dati_bancari`)"**: modulo di upload (tipologia estratto conto, contabile bonifico, contratto C/C; descrizione opzionale; file fino a 20MB) e tabella dei documenti archiviati con anteprima rapida, download e cancellazione gestita da Livewire (`uploadBankDocument`, `deleteBankDocument`, `getBankDocumentsProperty`). ## Output del Giro Operativo ESITO_205: riuscito -TASK_ID: task-banca-germanico-riconciliazione-unita-321 +TASK_ID: task-affitti-contatti-banca-documenti-stabilizzazione REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git BRANCH: stabilization/205-zero -COMMIT: ea30a24 +COMMIT: bafa27d FILE_O_AREE_TOCCATE: +- app/Filament/Pages/Affitti/GestioneAffitti.php +- app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php - app/Filament/Pages/Gescon/Ordinarie.php - app/Filament/Pages/UnitaImmobiliarePage.php -- app/Services/Contabilita/GermanicoBancaReconciliationService.php - app/Services/Gescon/GesconEstrattoContoService.php +- resources/views/filament/pages/affitti/gestione-affitti.blade.php - resources/views/filament/pages/gescon/section.blade.php - resources/views/filament/pages/unita-immobiliare.blade.php -- routes/web.php -- skill-netgescon/control-tower/CURRENT-205.md - tests/Feature/GermanicoBancaReconciliationTest.php +- skill-netgescon/control-tower/CURRENT-205.md TEST_ESEGUITI: -- ./vendor/bin/pest tests/Feature/GermanicoBancaReconciliationTest.php (5 passed, 57 assertions, 100% pass) -- ./vendor/bin/pest tests/Feature/GermanicoBancaReconciliationTest.php tests/Feature/UnitaEstrattoContoAtomicoTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/UnitaImmobiliarePageTest.php (19 passed, 215 assertions, 100% pass) +- ./vendor/bin/pest tests/Feature/GermanicoBancaReconciliationTest.php (10 passed, 82 assertions, 100% pass) +- ./vendor/bin/pest tests/Feature/GermanicoBancaReconciliationTest.php tests/Feature/UnitaEstrattoContoAtomicoTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/UnitaImmobiliarePageTest.php (24 passed, 242 assertions, 100% pass) - php artisan view:clear && php artisan view:cache (successo, 100% template compilati senza errori) GATE_STATISTICS: - BLADE_COMPILATION: 100% pulita senza errori di sintassi. - BENCHMARK_VERIFICATION: - - 0016 Condòmino Barone Michele B-4 = € 885,00 esatto (6 rate da 177,00 € di cui rata 1 saldata con bonifico 23/12/2025 da 960,79 €). - - 0013 Condòmino Spadavecchia A-1 = € 1.267,96 esatto (Passerella 965,60 + Cornicione 302,36). - - 0013 Inquilino Kiwa Cermet A-1 = € 731,30 esatto (0 straordinarie, 5 rate ordinarie 2026 con 560 € pagati e 731,30 € residuo). - - 0021 Condòmino Cefaloni Milizie 3 = € 847,69 esatto (Ordinaria 139,21 + Riscaldamento 135,00 + Straordinarie 573,48). -- BANCA_RECONCILIATION: - - 1.935 movimenti bancari importati e deduplicati da 7 file .xls. - - 1.684 movimenti bancari riconciliati (87,03%): 1.158 incassi e 526 spese. - - Ricevute PDF collegate via `Inc_da_ec` visibili e scaricabili. -- TEST_SUITE: 19 test Feature passati (215 asserzioni, 100% pass). + - 0016 Condòmino Barone Michele B-4 = € 885,00 esatto. + - 0013 Condòmino Spadavecchia A-1 = € 1.267,96 esatto. + - 0013 Inquilino Kiwa Cermet A-1 = € 731,30 esatto. + - 0021 Condòmino Cefaloni Milizie 3 = € 847,69 esatto. +- ISOLATION_VERIFICATION: + - Unità 287 (A-13 Lucaferri) linkata correttamente da incassi MDB. + - Unità 295 (A-21 Volpetti) priva di contaminazioni Stabile 0013 (0 straordinarie Passerella/Cornicione). +- CONTACTS_VERIFICATION: + - Unità 321 Barone Michele: recapiti Skynet rimossi; contatti reali presenti; residenza fallback su Via Germanico 96. +- PERMISSIONS_VERIFICATION: + - Cecilia Tordini (`amministratore`) accede alla Scheda Fattura Elettronica FE 255. +- AFFITTI_VERIFICATION: + - Gestione Affitti (`/admin-filament/affitti/gestione`) monta e renderizza con 6 tab operative e 0 errori 500. BLOCCO_DATI: no BLOCCO_CONTRATTO: no RISCHI_APERTI: nessuno @@ -75,12 +74,16 @@ ## Output del Giro Operativo ## Prossimo Passo per .200 (Validazione) - Eseguire il checkout del branch `stabilization/205-zero`. -- Eseguire la suite di test Pest (`./vendor/bin/pest tests/Feature/GermanicoBancaReconciliationTest.php tests/Feature/UnitaEstrattoContoAtomicoTest.php`). +- Eseguire la suite di test: + `./vendor/bin/pest tests/Feature/GermanicoBancaReconciliationTest.php tests/Feature/UnitaEstrattoContoAtomicoTest.php` - Verificare su http://192.168.0.205:8000: - 1. Ricerca Unità Germanico 96 (`/admin-filament/unita-immobiliare?unita_id=321`): - - Nel selettore unità compare correttamente `Scala B - Int. 4 [B-4] — Barone Michele / Francioni Claudia`. - - Nella scheda Estratto Conto: Totale Dovuto = **€ 885,00** (6 rate da 177,00 € con rata 1 saldata). - - Rata 1 riporta il badge bonifico `23/12/2025 (€ 960,79)` e il pulsante per visualizzare il PDF originale `Inc_EC_20260310_184901.pdf`. - - Sezione 5 "Movimenti Bancari Riconciliati" con tutti i bonifici e relative ricevute PDF. - 2. Cruscotto Banca Gescon (`/admin-filament/gescon/ordinarie?tab=banca`): - - Tab dedicato con 1.935 movimenti, 1.684 riconciliati (87,0%), filtri e collegamenti diretti all'estratto conto di ogni singola unità. + 1. Scheda Unità Barone Michele (`/admin-filament/unita-immobiliare?unita_id=321`): + - Recapiti mostrano i dati reali di Barone Michele / Cecilia Tordini e residenza Via Germanico 96. + - Cliccando su "📄 Ricevuta PDF" si apre il modale inline con l'anteprima del documento. + 2. Gestione Affitti (`/admin-filament/affitti/gestione`): + - Pagina accessibile con status 200 (nessun errore 500 $tabs). + 3. Scheda Fattura 255 (`/admin-filament/contabilita/fatture-ricevute/255`): + - Accessibile per l'utente amministratore Cecilia Tordini. + 4. Cruscotto Ordinarie Gescon (`/admin-filament/gescon/ordinarie?tab=banca`): + - Tab Banca con sezione "Documenti Ufficiali Bancari dello Stabile (dati_bancari)" con modulo upload e anteprima modale. + - Tab Incassi con Lucaferri Antonella (A-13) che linka all'Unità 287 (non più 295). diff --git a/tests/Feature/GermanicoBancaReconciliationTest.php b/tests/Feature/GermanicoBancaReconciliationTest.php index 810916c..4ad42db 100644 --- a/tests/Feature/GermanicoBancaReconciliationTest.php +++ b/tests/Feature/GermanicoBancaReconciliationTest.php @@ -42,15 +42,39 @@ $this->stabile = Stabile::find(19); DB::table('unita_immobiliari')->insertOrIgnore([ - 'id' => 321, - 'stabile_id' => 19, - 'scala' => 'B', - 'interno' => '4', - 'codice_unita' => '0016-B-4', - 'denominazione' => 'Barone Michele', - 'legacy_cond_id' => '47', - 'created_at' => now(), - 'updated_at' => now(), + [ + 'id' => 321, + 'stabile_id' => 19, + 'scala' => 'B', + 'interno' => '4', + 'codice_unita' => '0016-B-4', + 'denominazione' => 'Barone Michele', + 'legacy_cond_id' => '47', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'id' => 287, + 'stabile_id' => 19, + 'scala' => 'A', + 'interno' => '13', + 'codice_unita' => '0016-A-13', + 'denominazione' => 'Lucaferri Antonella', + 'legacy_cond_id' => '13', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'id' => 295, + 'stabile_id' => 19, + 'scala' => 'A', + 'interno' => '21', + 'codice_unita' => '0016-A-21', + 'denominazione' => 'Volpetti Elisabetta', + 'legacy_cond_id' => '21', + 'created_at' => now(), + 'updated_at' => now(), + ], ]); $this->unita = UnitaImmobiliare::find(321); @@ -202,3 +226,111 @@ $response->assertStatus(200); $response->assertHeader('content-type', 'application/pdf'); }); + +test('unit 321 multicanale contacts displays real barone michele data and germanico address fallback', function () { + auth()->login($this->user); + StabileContext::setActiveStabileId($this->user, 19); + request()->merge(['unita_id' => 321]); + + $page = new UnitaImmobiliarePage(); + $page->mount(); + + $recapiti = $page->recapitiMulticanaleTable; + expect($recapiti)->toBeArray()->and(count($recapiti))->toBeGreaterThan(0); + + $valori = collect($recapiti)->pluck('valore')->all(); + $valoriStr = implode(' | ', $valori); + + // Non deve mostrare i recapiti estranei di Skynet Solutions SRL + expect(str_contains($valoriStr, 'gmpskynet@gmail.com'))->toBeFalse(); + expect(str_contains($valoriStr, '33583314342'))->toBeFalse(); + expect(str_contains($valoriStr, 'Via Andrea Bafilr'))->toBeFalse(); + + // Deve mostrare i recapiti reali di Barone Michele / Cecilia Tordini + expect($valoriStr)->toContain('barone.michele@gmail.com') + ->and($valoriStr)->toContain('3388155553'); + + // Indirizzo di residenza assente nel profilo cade sullo stabile + expect($valoriStr)->toContain('Via Germanico 96 00192 Roma (RM)'); +}); + +test('gestione affitti page mounts and renders cleanly without 500 error', function () { + $component = Livewire::actingAs($this->user) + ->test(\App\Filament\Pages\Affitti\GestioneAffitti::class); + + $component->assertSuccessful(); + expect($component->instance()->tabs)->toBeArray() + ->and(count($component->instance()->tabs))->toBe(6); +}); + +test('electronic invoice fe 255 allows access to amministratore user', function () { + $adminUser = User::factory()->create(); + \Spatie\Permission\Models\Role::firstOrCreate(['name' => 'amministratore', 'guard_name' => 'web']); + $adminUser->assignRole('amministratore'); + + auth()->login($adminUser); + + expect(\App\Filament\Pages\Contabilita\FatturaElettronicaScheda::canAccess())->toBeTrue(); +}); + +test('ordinarie page bank table links lucaferri a13 to unit 287 and unit 295 does not leak 0013 straordinarie', function () { + auth()->login($this->user); + StabileContext::setActiveStabileId($this->user, 19); + + $page = new Ordinarie(); + $page->mount(); + $page->search = 'Lucaferri'; + + // Verifica matching incassi MDB: Lucaferri Antonella (A-13) punta all'unita 287 + $itemA13 = $page->incassi->first(); + expect($itemA13)->not->toBeNull(); + expect(str_contains((string) $itemA13->nominativo_pagante, 'Lucaferri'))->toBeTrue(); + expect(str_contains((string) $itemA13->unita_ec_url, 'unita_id=287'))->toBeTrue(); + expect(str_contains((string) $itemA13->unita_ec_url, 'unita_id=295'))->toBeFalse(); + + // Verifica che l'estratto conto dell'unita 295 non contenga dati dello Stabile 0013 Spadavecchia + $unita295 = UnitaImmobiliare::findOrFail(295); + $service = app(GesconEstrattoContoService::class); + $ec295 = $service->getEstrattoConto($unita295, 'C', true); + + expect($ec295['codice_stabile'])->toBe('0016'); + + $straordTitles = collect($ec295['gestioni_straordinarie'] ?? [])->pluck('titolo')->all(); + $straordStr = implode(' | ', $straordTitles); + + expect(str_contains($straordStr, 'PASSERELLA'))->toBeFalse(); + expect(str_contains($straordStr, 'CORNICIONE'))->toBeFalse(); + expect(str_contains($straordStr, 'SPADAVECCHIA'))->toBeFalse(); +}); + +test('bank documents upload list and delete functionality in ordinarie', function () { + auth()->login($this->user); + StabileContext::setActiveStabileId($this->user, 19); + + $page = new Ordinarie(); + $page->mount(); + + $initialCount = $page->bankDocuments->count(); + + $fakeFile = \Illuminate\Http\UploadedFile::fake()->create('estratto_conto_fucino_2025.pdf', 120, 'application/pdf'); + $page->bankDocumentTipo = 'estratto_conto'; + $page->bankDocumentDescrizione = 'Estratto Conto Ufficiale Q4 2025'; + $page->bankDocumentFile = $fakeFile; + + $page->uploadBankDocument(); + + $docs = $page->getBankDocumentsProperty(); + expect($docs->count())->toBe($initialCount + 1); + + $uploaded = $docs->first(); + expect($uploaded->nome_originale)->toBe('estratto_conto_fucino_2025.pdf') + ->and($uploaded->categoria)->toBe('bancari') + ->and($uploaded->stabile_id)->toBe(19); + + // Test eliminazione documento + $page->deleteBankDocument($uploaded->id); + + $docsAfterDelete = $page->getBankDocumentsProperty(); + expect($docsAfterDelete->count())->toBe($initialCount); +}); +