fix(affitti,contatti,banca): fix affitti 500 tabs, barone contacts, fe 255 access, cross-stabile isolation, bank docs and preview modals

This commit is contained in:
michele 2026-09-14 16:15:01 +02:00
parent a79598e66a
commit f4d2050a92
10 changed files with 995 additions and 128 deletions

View File

@ -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();

View File

@ -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);
}

View File

@ -1,6 +1,7 @@
<?php
namespace App\Filament\Pages\Gescon;
use App\Models\DocumentoStabile;
use App\Models\User;
use App\Models\UserSetting;
use App\Services\Contabilita\PreventivoOrdinarioPreviewService;
@ -15,12 +16,19 @@
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Livewire\WithFileUploads;
use Livewire\WithPagination;
use UnitEnum;
class Ordinarie extends Page
{
use WithPagination;
use WithFileUploads;
public $bankDocumentFile;
public ?string $bankDocumentDescrizione = null;
public ?string $bankDocumentTipo = 'estratto_conto';
protected static ?string $navigationLabel = 'Gest. ordinaria';
@ -1973,9 +1981,6 @@ public function getIncassiFromMdb()
$condominRows = $gesconService->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();
}
}

View File

@ -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;
}

View File

@ -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' => [],
];
}
}

View File

@ -70,7 +70,7 @@ class="inline-flex items-center gap-1 rounded-lg border border-amber-300 bg-ambe
</div>
{{-- KPI SUMMARY CARDS --}}
@php($kpi = $this->kpiStats)
<?php $kpi = $this->kpiStats; ?>
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-5">
<div class="rounded-xl border border-slate-200 bg-gradient-to-br from-white to-slate-50 p-3 shadow-xs">
<div class="flex items-center justify-between text-xs font-medium text-slate-500">
@ -156,7 +156,7 @@ class="inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-slat
<span>Scheda Unità</span>
</a>
@endif
@php($resolvedPersonaId = $this->resolvePersonaIdForSelected())
<?php $resolvedPersonaId = $this->resolvePersonaIdForSelected(); ?>
@if($resolvedPersonaId)
<a href="{{ \App\Filament\Pages\Gescon\AnagraficaUnica::getUrl(panel: 'admin-filament') . '?persona_id=' . $resolvedPersonaId }}"
class="inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-slate-50 px-2.5 py-1 text-xs font-medium text-slate-700 hover:bg-slate-100">
@ -170,17 +170,7 @@ class="inline-flex items-center gap-1 rounded-lg border border-slate-200 bg-slat
{{-- PILL TAB BAR --}}
<div class="flex flex-wrap items-center gap-1.5 border-b border-slate-200 pb-2">
@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)
<button type="button" wire:click="setTab('{{ $key }}')"
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition {{ $tab === $key ? 'bg-indigo-600 text-white shadow-xs' : 'bg-slate-100 text-slate-600 hover:bg-slate-200 hover:text-slate-900' }}">
<span>{{ $tabData['icon'] }}</span>
@ -526,7 +516,7 @@ class="inline-flex items-center gap-1 rounded-lg bg-indigo-50 border border-indi
<option value="{{ $id }}">{{ $label }}</option>
@endforeach
</select>
@php($currentPId = $this->resolvePersonaIdForSelected())
<?php $currentPId = $this->resolvePersonaIdForSelected(); ?>
@if($currentPId)
<a href="{{ \App\Filament\Pages\Gescon\AnagraficaUnica::getUrl(panel: 'admin-filament') . '?persona_id=' . $currentPId }}"
target="_blank"
@ -786,8 +776,10 @@ class="inline-flex items-center gap-1 rounded-lg border border-amber-300 bg-ambe
</thead>
<tbody class="divide-y divide-slate-100 bg-white">
@forelse($canoniRighe as $r)
@php($d = (float)($r['totale_dovuto'] ?? 0))
@php($p = (float)($r['totale_pagato'] ?? 0))
<?php
$d = (float)($r['totale_dovuto'] ?? 0);
$p = (float)($r['totale_pagato'] ?? 0);
?>
<tr class="hover:bg-slate-50/70 transition">
<td class="px-3 py-2 font-medium text-slate-800">{{ $r['mese_descrizione'] ?? ($r['mese'] ?? '') }} {{ $r['anno'] ?? '' }}</td>
<td class="px-3 py-2 text-slate-600">

View File

@ -25,7 +25,21 @@
@endphp
<x-filament-panels::page>
<div class="mx-auto max-w-7xl space-y-4">
<div class="mx-auto max-w-7xl space-y-4" x-data="{
previewModalOpen: false,
previewDocUrl: '',
previewDocTitle: '',
openPreview(url, title) {
this.previewDocUrl = url;
this.previewDocTitle = title || 'Visualizzazione Documento';
this.previewModalOpen = true;
},
closePreview() {
this.previewModalOpen = false;
this.previewDocUrl = '';
this.previewDocTitle = '';
}
}">
<x-filament::section>
<x-slot name="heading">{{ $meta['title'] }}</x-slot>
@if(!empty($meta['desc']))
@ -965,7 +979,10 @@
🏦 {{ $row->movimento_banca['data'] }} ( {{ number_format($row->movimento_banca['importo'], 2, ',', '.') }})
</span>
@if(!empty($row->movimento_banca['pdf_url']))
<a href="{{ $row->movimento_banca['pdf_url'] }}" target="_blank" class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
<a href="{{ $row->movimento_banca['pdf_url'] }}"
target="_blank"
@click.prevent="openPreview('{{ $row->movimento_banca['pdf_url'] }}', 'Ricevuta Incasso - {{ addslashes($row->nominativo_pagante ?? '') }}')"
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
📄 Ricevuta PDF
</a>
@endif
@ -1548,6 +1565,7 @@ class="inline-flex items-center gap-1 px-2 py-1 rounded-lg bg-amber-50 hover:bg-
@if(!empty($row->match_pdf_url))
<a href="{{ $row->match_pdf_url }}"
target="_blank"
@click.prevent="openPreview('{{ $row->match_pdf_url }}', 'Ricevuta Incasso #{{ $row->match_protocollo ?? '' }} - {{ addslashes($row->match_condomino ?? 'Condomino') }}')"
title="Visualizza ricevuta incasso PDF archiviata"
class="inline-flex items-center gap-1 px-2 py-1 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200 text-[10px] font-semibold transition">
<x-filament::icon icon="heroicon-m-document-arrow-down" class="w-3.5 h-3.5 text-indigo-600" />
@ -1572,6 +1590,152 @@ class="inline-flex items-center gap-1 px-2 py-1 rounded-lg bg-indigo-50 hover:bg
</div>
@endif
</div>
{{-- Sezione Documenti Ufficiali Bancari dello Stabile (dati_bancari) --}}
<div class="rounded-2xl border border-slate-200 bg-white p-5 shadow-xs space-y-4">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 border-b border-slate-100 pb-3">
<div class="flex items-center gap-2.5">
<span class="text-xl">📁</span>
<div>
<h4 class="text-sm font-bold text-slate-800">
Documenti Ufficiali Bancari dello Stabile (dati_bancari)
</h4>
<p class="text-xs text-slate-500">
Estratti conto bancari ufficiali in PDF, contabili bonifici/F24, contratti e documentazione di conto corrente archiviati.
</p>
</div>
</div>
<span class="inline-flex items-center px-2.5 py-1 rounded-lg text-xs font-medium bg-slate-100 text-slate-600">
{{ count($this->bankDocuments ?? []) }} documenti archiviati
</span>
</div>
{{-- Form Caricamento Nuovo Documento Bancario --}}
<div class="rounded-xl border border-dashed border-indigo-300 bg-indigo-50/40 p-4">
<h5 class="text-xs font-bold text-indigo-950 mb-2 flex items-center gap-1.5">
<x-filament::icon icon="heroicon-m-arrow-up-tray" class="w-4 h-4 text-indigo-600" />
<span>Allega Documento Ufficiale al Conto Bancario</span>
</h5>
<div class="grid grid-cols-1 sm:grid-cols-12 gap-3 items-end">
<div class="sm:col-span-3">
<label class="block text-[11px] font-medium text-slate-700 mb-1">Tipologia Documento</label>
<select wire:model="bankDocumentTipo" class="w-full rounded-lg border-slate-300 text-xs py-1.5 px-2 bg-white">
<option value="estratto_conto">Estratto Conto Ufficiale (PDF)</option>
<option value="contabile_bonifico">Contabile Bonifico / F24</option>
<option value="contratto_cc">Contratto Apertura C/C</option>
<option value="altro">Altro Documento Bancario</option>
</select>
</div>
<div class="sm:col-span-4">
<label class="block text-[11px] font-medium text-slate-700 mb-1">Descrizione / Note (Opzionale)</label>
<input type="text" wire:model="bankDocumentDescrizione" placeholder="Es. Estratto conto Q4 2025 Banca del Fucino" class="w-full rounded-lg border-slate-300 text-xs py-1.5 px-2.5 bg-white" />
</div>
<div class="sm:col-span-3">
<label class="block text-[11px] font-medium text-slate-700 mb-1">File (PDF, XLS, PNG, JPG)</label>
<input type="file" wire:model="bankDocumentFile" class="w-full text-xs text-slate-600 file:mr-2 file:py-1 file:px-2 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-indigo-600 file:text-white hover:file:bg-indigo-700" />
</div>
<div class="sm:col-span-2 flex items-end">
<x-filament::button
size="sm"
color="primary"
wire:click="uploadBankDocument"
wire:loading.attr="disabled"
class="w-full justify-center"
>
<span wire:loading.remove class="flex items-center gap-1">
<x-filament::icon icon="heroicon-m-arrow-up-tray" class="w-3.5 h-3.5" />
<span>Carica</span>
</span>
<span wire:loading class="flex items-center gap-1">
<x-filament::icon icon="heroicon-m-arrow-path" class="w-3.5 h-3.5 animate-spin" />
<span>Caricamento...</span>
</span>
</x-filament::button>
</div>
</div>
@error('bankDocumentFile')
<p class="text-xs text-rose-600 mt-1.5 font-medium">{{ $message }}</p>
@enderror
</div>
{{-- Elenco Documenti Bancari Caricati --}}
@php
$bDocs = $this->bankDocuments ?? collect();
@endphp
@if($bDocs->isEmpty())
<div class="text-center py-6 text-xs text-slate-400 italic bg-slate-50 rounded-xl border border-slate-100">
Nessun documento bancario ufficiale archiviato per questo stabile. Utilizza il modulo sopra per allegare estratti conto ufficiali o contabili.
</div>
@else
<div class="overflow-x-auto rounded-xl border border-slate-200">
<table class="w-full text-xs text-left border-collapse">
<thead>
<tr class="bg-slate-50 text-slate-600 font-semibold border-b border-slate-200">
<th class="px-3 py-2">Data Caricamento</th>
<th class="px-3 py-2">Tipologia</th>
<th class="px-4 py-2">Descrizione</th>
<th class="px-3 py-2">File Originale</th>
<th class="px-3 py-2">Dimensione</th>
<th class="px-3 py-2 text-right">Azioni</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 bg-white">
@foreach($bDocs as $doc)
@php
$fileUrl = asset('storage/' . ltrim($doc->percorso_file, '/'));
@endphp
<tr class="hover:bg-slate-50/70 transition">
<td class="px-3 py-2 text-slate-500 font-mono whitespace-nowrap">
{{ $doc->created_at ? $doc->created_at->format('d/m/Y H:i') : '—' }}
</td>
<td class="px-3 py-2 whitespace-nowrap">
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 border border-indigo-100">
{{ ucfirst(str_replace('_', ' ', $doc->tags ? explode(',', $doc->tags)[3] ?? 'estratto' : 'estratto')) }}
</span>
</td>
<td class="px-4 py-2 text-slate-900 font-medium">
{{ $doc->descrizione }}
</td>
<td class="px-3 py-2 text-slate-600 font-mono">
{{ $doc->nome_originale }}
</td>
<td class="px-3 py-2 text-slate-500 font-mono whitespace-nowrap">
{{ $doc->dimensione_formattata }}
</td>
<td class="px-3 py-2 text-right whitespace-nowrap">
<div class="flex items-center justify-end gap-1.5">
<button type="button"
@click="openPreview('{{ $fileUrl }}', '{{ addslashes($doc->descrizione ?: $doc->nome_originale) }}')"
class="inline-flex items-center gap-1 px-2 py-1 rounded bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200 text-[10px] font-semibold transition"
title="Anteprima rapida nel modale">
<x-filament::icon icon="heroicon-m-eye" class="w-3.5 h-3.5 text-indigo-600" />
<span>Anteprima</span>
</button>
<a href="{{ $fileUrl }}"
download="{{ $doc->nome_originale }}"
target="_blank"
class="inline-flex items-center gap-1 px-2 py-1 rounded bg-slate-50 hover:bg-slate-100 text-slate-700 border border-slate-200 text-[10px] font-semibold transition"
title="Scarica file">
<x-filament::icon icon="heroicon-m-arrow-down-tray" class="w-3.5 h-3.5 text-slate-600" />
<span>Scarica</span>
</a>
<button type="button"
wire:click="deleteBankDocument({{ $doc->id }})"
wire:confirm="Sei sicuro di voler eliminare questo documento bancario?"
class="inline-flex items-center gap-1 px-2 py-1 rounded bg-rose-50 hover:bg-rose-100 text-rose-700 border border-rose-200 text-[10px] font-semibold transition"
title="Elimina documento">
<x-filament::icon icon="heroicon-m-trash" class="w-3.5 h-3.5 text-rose-600" />
<span>Elimina</span>
</button>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
</div>
@endif
@ -1954,5 +2118,30 @@ class="fi-input block w-28 text-right text-xs ml-auto"
@endif
</x-filament::section>
{{-- Modal Anteprima Documento Inline --}}
<template x-if="previewModalOpen">
<div class="fixed inset-0 z-50 overflow-y-auto bg-gray-900/60 backdrop-blur-xs flex items-center justify-center p-4" x-cloak>
<div class="relative w-full max-w-4xl bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between px-4 py-3 bg-gray-100 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<div class="font-bold text-sm text-gray-800 dark:text-gray-200 truncate flex items-center gap-2">
<span class="text-indigo-600">📄</span>
<span x-text="previewDocTitle"></span>
</div>
<div class="flex items-center gap-2">
<a :href="previewDocUrl" target="_blank" download class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-semibold text-indigo-700 bg-indigo-50 hover:bg-indigo-100 rounded-lg border border-indigo-200 transition">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
<span>Scarica</span>
</a>
<button @click="closePreview()" type="button" class="p-1 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-200 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
</div>
<div class="flex-1 p-2 bg-gray-100 dark:bg-gray-950 flex items-center justify-center min-h-[450px]">
<iframe :src="previewDocUrl" class="w-full h-[70vh] rounded-lg border border-gray-300 dark:border-gray-700 bg-white"></iframe>
</div>
</div>
</div>
</template>
</div>
</x-filament-panels::page>

View File

@ -1,5 +1,19 @@
<x-filament-panels::page>
<div class="w-full max-w-full space-y-4 px-2 lg:px-6">
<div class="w-full max-w-full space-y-4 px-2 lg:px-6" x-data="{
receiptModalOpen: false,
receiptModalUrl: '',
receiptModalTitle: '',
openReceiptModal(url, title) {
this.receiptModalUrl = url;
this.receiptModalTitle = title || 'Ricevuta di Pagamento';
this.receiptModalOpen = true;
},
closeReceiptModal() {
this.receiptModalOpen = false;
this.receiptModalUrl = '';
this.receiptModalTitle = '';
}
}">
{{-- BARRA NAVIGAZIONE UNIFICATA E COMPATTA --}}
<div class="mb-2 flex flex-wrap items-center justify-between gap-2 rounded-xl border border-slate-200 bg-white/80 p-2 shadow-xs backdrop-blur-xs">
<div class="flex flex-wrap items-center gap-1.5">
@ -824,14 +838,20 @@ class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-800 hove
🏦 Bonifico {{ $rata['movimento_bancario']['data'] ?? '' }} ( {{ number_format($rata['movimento_bancario']['importo'] ?? 0, 2, ',', '.') }})
</span>
@if(!empty($rata['ricevuta_url']))
<a href="{{ $rata['ricevuta_url'] }}" target="_blank" class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
<a href="{{ $rata['ricevuta_url'] }}"
target="_blank"
@click.prevent="openReceiptModal('{{ $rata['ricevuta_url'] }}', 'Ricevuta Pagamento {{ $rata['ricevuta_file'] ?? '' }}')"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
📄 Ricevuta PDF
</a>
@endif
</div>
@elseif(!empty($rata['ricevuta_url']))
<div class="mt-1">
<a href="{{ $rata['ricevuta_url'] }}" target="_blank" class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
<a href="{{ $rata['ricevuta_url'] }}"
target="_blank"
@click.prevent="openReceiptModal('{{ $rata['ricevuta_url'] }}', 'Ricevuta Pagamento {{ $rata['ricevuta_file'] ?? '' }}')"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
📄 Ricevuta PDF
</a>
</div>
@ -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, ',', '.') }})
</span>
@if(!empty($sr['ricevuta_url']))
<a href="{{ $sr['ricevuta_url'] }}" target="_blank" class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
<a href="{{ $sr['ricevuta_url'] }}"
target="_blank"
@click.prevent="openReceiptModal('{{ $sr['ricevuta_url'] }}', 'Ricevuta Pagamento {{ $sr['ricevuta_file'] ?? '' }}')"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
📄 Ricevuta PDF
</a>
@endif
</div>
@elseif(!empty($sr['ricevuta_url']))
<div class="mt-1">
<a href="{{ $sr['ricevuta_url'] }}" target="_blank" class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
<a href="{{ $sr['ricevuta_url'] }}"
target="_blank"
@click.prevent="openReceiptModal('{{ $sr['ricevuta_url'] }}', 'Ricevuta Pagamento {{ $sr['ricevuta_file'] ?? '' }}')"
class="inline-flex items-center gap-1 px-2 py-0.5 rounded text-[10px] font-semibold bg-indigo-50 text-indigo-700 hover:bg-indigo-100 border border-indigo-200 transition">
📄 Ricevuta PDF
</a>
</div>
@ -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']))
<a href="{{ $mb['pdf_url'] }}"
target="_blank"
@click.prevent="openReceiptModal('{{ $mb['pdf_url'] }}', 'Ricevuta {{ $mb['pdf_ricevuta'] ?? '' }}')"
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200 text-[11px] font-semibold transition">
<x-filament::icon icon="heroicon-m-document-arrow-down" class="w-3.5 h-3.5 text-indigo-600" />
<span>{{ $mb['pdf_ricevuta'] }}</span>
@ -2152,5 +2179,31 @@ class="px-4 py-1.5 text-xs font-semibold text-gray-600 dark:text-gray-300 hover:
</div>
</div>
@endif
{{-- Modal Anteprima Ricevuta Pagamento Inc_EC / Documento --}}
<template x-if="receiptModalOpen">
<div class="fixed inset-0 z-50 overflow-y-auto bg-gray-900/60 backdrop-blur-xs flex items-center justify-center p-4" x-cloak>
<div class="relative w-full max-w-4xl bg-white dark:bg-gray-800 rounded-2xl shadow-2xl border border-gray-200 dark:border-gray-700 overflow-hidden flex flex-col max-h-[90vh]">
<div class="flex items-center justify-between px-4 py-3 bg-gray-100 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<div class="font-bold text-sm text-gray-800 dark:text-gray-200 truncate flex items-center gap-2">
<x-heroicon-m-document-text class="w-4 h-4 text-indigo-600" />
<span x-text="receiptModalTitle"></span>
</div>
<div class="flex items-center gap-2">
<a :href="receiptModalUrl" target="_blank" download class="inline-flex items-center gap-1 px-2.5 py-1 text-xs font-semibold text-indigo-700 bg-indigo-50 hover:bg-indigo-100 rounded-lg border border-indigo-200 transition">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"></path></svg>
<span>Apri / Scarica</span>
</a>
<button @click="closeReceiptModal()" type="button" class="p-1 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-200 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
</div>
<div class="flex-1 p-2 bg-gray-100 dark:bg-gray-950 flex items-center justify-center min-h-[450px]">
<iframe :src="receiptModalUrl" class="w-full h-[70vh] rounded-lg border border-gray-300 dark:border-gray-700 bg-white"></iframe>
</div>
</div>
</div>
</template>
</div>
</x-filament-panels::page>

View File

@ -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).

View File

@ -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);
});