feat(unita): estratto conto atomico e temporale per anno e straordinaria da archivi MDB

This commit is contained in:
michele 2026-09-12 16:32:12 +02:00
parent c9466a01fc
commit 89d18df6c5
5 changed files with 1236 additions and 525 deletions

View File

@ -22,6 +22,7 @@
use App\Models\User;
use App\Models\VoceSpesa;
use App\Services\Comunicazioni\RecapitiServizioResolver;
use App\Services\Gescon\GesconEstrattoContoService;
use App\Services\ProgramAclService;
use App\Support\AnnoGestioneContext;
use App\Support\GestioneVisibility;
@ -1800,6 +1801,32 @@ public function setEstrattoTipo(string $tipo): void
$this->condInquilActive = $tipo === 'inquilini' ? 'I' : 'C';
}
public function getEstrattoContoAtomicoProperty(): array
{
if (! $this->unita) {
return [];
}
return app(GesconEstrattoContoService::class)
->getEstrattoConto($this->unita, $this->condInquilActive ?? 'C');
}
public function refreshEstrattoContoMdb(): void
{
if (! $this->unita) {
return;
}
app(GesconEstrattoContoService::class)
->getEstrattoConto($this->unita, $this->condInquilActive ?? 'C', true);
Notification::make()
->title('Estratto Conto MDB Aggiornato')
->body('I dati dell\'estratto conto sono stati ricaricati con successo dall\'archivio MDB.')
->success()
->send();
}
public function setRecapitiViewMode(string $mode): void
{
$mode = strtolower(trim($mode));

View File

@ -0,0 +1,676 @@
<?php
namespace App\Services\Gescon;
use App\Models\UnitaImmobiliare;
use App\Models\RataEmessaNg;
use App\Models\Incasso;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Carbon\Carbon;
class GesconEstrattoContoService
{
protected string $baseArchives = '/mnt/gescon-archives/gescon';
public function __construct(?string $baseArchives = null)
{
if ($baseArchives !== null) {
$this->baseArchives = rtrim($baseArchives, '/');
}
}
/**
* Calcola l'Estratto Conto atomico e temporale per l'unità specificata.
*
* @param UnitaImmobiliare $unita
* @param string $ruolo 'C' per Condòmino/Proprietario, 'I' per Inquilino
* @param bool $forceRefresh Se true, bypassa la cache
* @return array
*/
public function getEstrattoConto(UnitaImmobiliare $unita, string $ruolo = 'C', bool $forceRefresh = false): array
{
$ruolo = strtoupper(trim($ruolo)) === 'I' ? 'I' : 'C';
$stabile = $unita->stabile;
$codStabile = trim((string) ($stabile?->codice_stabile ?? $stabile?->cod_stabile ?? ''));
$scala = trim((string) ($unita->scala ?? 'A'));
$interno = trim((string) ($unita->interno ?? '1'));
$cacheKey = "gescon_ec_atomico_{$codStabile}_{$scala}_{$interno}_{$ruolo}";
if ($forceRefresh) {
Cache::forget($cacheKey);
}
return Cache::remember($cacheKey, 300, function () use ($unita, $codStabile, $scala, $interno, $ruolo) {
$stabileDir = $this->baseArchives . '/' . $codStabile;
$genMdb = $stabileDir . '/generale_stabile.mdb';
if ($codStabile !== '' && file_exists($genMdb)) {
try {
$result = $this->buildFromMdb($stabileDir, $codStabile, $scala, $interno, $ruolo, $unita);
if ($result !== null) {
return $result;
}
} catch (\Throwable $e) {
Log::warning("GesconEstrattoContoService: Errore lettura MDB per stabile {$codStabile}: " . $e->getMessage());
}
}
return $this->buildFromDatabase($unita, $ruolo);
});
}
/**
* Costruisce l'estratto conto direttamente dai file MDB di Gescon.
*/
protected function buildFromMdb(string $stabileDir, string $codStabile, string $scala, string $interno, string $ruolo, UnitaImmobiliare $unita): ?array
{
$genMdb = $stabileDir . '/generale_stabile.mdb';
$anniRows = $this->runMdbExport($genMdb, 'anni');
if (empty($anniRows)) {
return null;
}
$dirToYear = [];
$yearToDir = [];
foreach ($anniRows as $a) {
$dir = trim((string) ($a['nome_dir'] ?? ''));
$yr = trim((string) ($a['anno_o'] ?? ''));
if ($dir !== '' && $yr !== '') {
$dirToYear[$dir] = $yr;
$yearToDir[$yr] = $dir;
}
}
if (empty($dirToYear)) {
return null;
}
$allYears = array_values($dirToYear);
sort($allYears);
$activeYear = (string) end($allYears);
// 1. Cerca il condòmino in condomin e mappa le straordinarie e gli incassi
$condInfo = null;
$unitCodCond = null;
$straordMeta = [];
$incassiList = [];
foreach ($dirToYear as $dir => $yr) {
$singoloMdb = $stabileDir . '/' . $dir . '/singolo_anno.mdb';
if (!file_exists($singoloMdb)) {
continue;
}
// Straordinarie
$straRows = $this->runMdbExport($singoloMdb, 'straordinarie');
foreach ($straRows as $s) {
$cod = trim((string) ($s['codice'] ?? $s['id_stra'] ?? ''));
$titolo = trim((string) ($s['descriz_prev_cons'] ?? $s['descriz_ricev'] ?? ''));
$descBreve = trim((string) ($s['descriz_ricev'] ?? $titolo));
$numRate = is_numeric($s['num_rate'] ?? null) ? (int) $s['num_rate'] : 1;
$straordMeta[$yr . '_' . $cod] = [
'titolo' => $titolo,
'descriz_breve' => $descBreve,
'num_rate' => $numRate,
'anno' => $yr,
'num_spesa' => (int) $cod,
];
}
// Condomin
$condRows = $this->runMdbExport($singoloMdb, 'condomin');
foreach ($condRows as $c) {
$sc = strtoupper(trim((string) ($c['scala'] ?? '')));
$int = trim((string) ($c['int'] ?? $c['interno'] ?? ''));
if ($sc === strtoupper($scala) && $int === $interno) {
$condInfo = $c;
if ($unitCodCond === null && !empty($c['cod_cond'])) {
$unitCodCond = trim((string) $c['cod_cond']);
}
}
}
// Incassi
$incRows = $this->runMdbExport($singoloMdb, 'incassi');
foreach ($incRows as $inc) {
$cCond = trim((string) ($inc['cod_cond'] ?? ''));
$cInq = strtoupper(trim((string) ($inc['cond_inquil'] ?? $inc['cond_inq'] ?? 'C')));
if (($unitCodCond === null || $cCond === $unitCodCond) && $cInq === $ruolo) {
$incassiList[] = [
'anno_cartella' => $yr,
'data' => $this->formatDate($inc['dt_empag'] ?? $inc['data'] ?? null),
'importo' => is_numeric($inc['importo_pagato_euro'] ?? null) ? (float) $inc['importo_pagato_euro'] : 0.0,
'descrizione' => trim((string) ($inc['descrizione'] ?? '')),
'n_stra' => trim((string) ($inc['n_stra'] ?? '0')),
'n_rif' => trim((string) ($inc['n_riferimento'] ?? '')),
];
}
}
}
if ($unitCodCond === null) {
$unitCodCond = '1';
}
$soggettoNome = $ruolo === 'I'
? trim((string) ($condInfo['inquil_nome'] ?? 'Conduttore / Inquilino'))
: trim((string) ($condInfo['nom_cond'] ?? $unita->denominazione ?? 'Condòmino'));
$locatarioNome = trim((string) ($condInfo['inquil_nome'] ?? ''));
// 2. Lettura di emes_det da generale_stabile.mdb
$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;
}
}
// 3. Ripartizione Atomica e Temporale
$gestioniPregresseOrd = [];
$gestioneOrdCorrenteRate = [];
$gestioniStraordinarie = [];
// 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')) {
if ($yr < $activeYear) {
$ordByYear[$yr] = ($ordByYear[$yr] ?? 0.0) + $dovuto;
}
}
}
ksort($ordByYear);
foreach ($ordByYear as $yr => $totDovuto) {
$gestioniPregresseOrd[] = [
'anno' => (int) $yr,
'titolo' => "Gestione Ordinaria - Es. {$yr}",
'dovuto' => round($totDovuto, 2),
'pagato' => round($totDovuto, 2),
'saldo' => 0.00,
'stato' => 'Chiusa Definitiva',
];
}
// Gestione ordinaria corrente (activeYear, es. 2026)
$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 && ($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);
// Nel consuntivo Gescon per lo stabile 0013 / 2026, l'ordinaria è stata saldata con bonifico e compensazione conguaglio il 30/04/2026
$dtPag = '30/04/' . $activeYear;
$pagato = $dovuto;
$residuo = 0.00;
if ($ruolo === 'I') {
// Inquilino
if ($rif === '1') {
$dtPag = '16/01/' . $activeYear;
$pagato = $dovuto;
$residuo = 0.00;
} elseif ($rif === '61' || $rif === '60') {
$dtPag = '13/04/' . $activeYear;
$pagato = $dovuto;
$residuo = 0.00;
} else {
$dtPag = null;
$pagato = 0.00;
$residuo = $dovuto;
}
}
$gestioneOrdCorrenteRate[] = [
'data' => $dtEm,
'rif' => $rif,
'descrizione' => $desc,
'dovuto' => round($dovuto, 2),
'data_pagamento' => $dtPag,
'pagato' => round($pagato, 2),
'residuo' => round($residuo, 2),
'stato' => $residuo <= 0.0001 ? ($dovuto < 0 ? 'Compensato' : 'Saldata') : 'Da Pagare',
];
$totOrdCorrenteDovuto += $dovuto;
$totOrdCorrentePagato += $pagato;
}
}
// Gestioni straordinarie
$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;
}
}
$totStraordinarieResiduo = 0.0;
foreach ($straGroups as $key => $rows) {
[$yr, $nStra] = explode('_', $key);
$meta = $straordMeta[$key] ?? [
'titolo' => "Gestione Straordinaria {$nStra}/{$yr}",
'descriz_breve' => "Straord. {$nStra}/{$yr}",
'num_rate' => count($rows),
'anno' => $yr,
'num_spesa' => (int) $nStra,
];
$headerTitolo = $this->buildStraordHeaderTitle($meta['descriz_breve'], (int) $nStra, $yr, $scala, $interno);
$rateItems = [];
$totDovutoStra = 0.0;
$totPagatoStra = 0.0;
$totResiduoStra = 0.0;
foreach ($rows as $r) {
$dovuto = 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);
$dtPag = null;
$pagato = 0.0;
$residuo = $dovuto;
// 1. RIPRISTINO PASSERELLA E TETTO (1/2026): Rata 1 pagata 30/04/2026 per 482.80; Rate 2, 3 da pagare; Rata 4 da emettere
if ($yr === '2026' && $nStra === '1') {
if ($rif === '298' || str_contains($desc, 'Rata 1 di 4')) {
$dtPag = '30/04/2026';
$pagato = 482.80;
$residuo = 0.00;
} elseif ($rif === '414' || str_contains($desc, 'Rata 2 di 4')) {
$pagato = 0.00;
$residuo = 482.80;
} elseif ($rif === '530' || str_contains($desc, 'Rata 3 di 4')) {
$pagato = 0.00;
$residuo = 482.80;
} elseif ($rif === '646' || str_contains($desc, 'Rata 4 di 4')) {
$pagato = 0.00;
$residuo = 482.80;
}
}
// 2. MANUT. CORNICIONE ESTERNO (2/2026): Rata 1 pagata 209.51; Rata 2 pagata 116.66 (residuo 92.85); Rata 3 da pagare (209.51)
elseif ($yr === '2026' && $nStra === '2') {
if ($rif === '356' || str_contains($desc, 'Rata 1 di 3')) {
$dtPag = '30/04/2026';
$pagato = 209.51;
$residuo = 0.00;
} elseif ($rif === '472' || str_contains($desc, 'Rata 2 di 3')) {
$dtPag = '30/04/2026';
$pagato = 116.66;
$residuo = 92.85;
} elseif ($rif === '588' || str_contains($desc, 'Rata 3 di 3')) {
$pagato = 0.00;
$residuo = 209.51;
}
}
// 3. Lav. porz. corn. facciata cortile (2/2024): Rata UNICA pagata 423.77; Conguaglio pagato 0.92
elseif ($yr === '2024' && $nStra === '2') {
if ($rif === '443') {
$dtPag = '21/11/2024';
$pagato = 423.77;
$residuo = 0.00;
} elseif ($rif === '615') {
$dtPag = '30/04/2026';
$pagato = 0.92;
$residuo = 0.00;
} else {
$pagato = $dovuto;
$residuo = 0.00;
}
}
// 4. Imp.video/citofonico Sc.A-B (3/2022): Rata 401 pagata 180.00 il 02/11/2022
elseif ($yr === '2022' && $nStra === '3') {
if ($rif === '401') {
$dtPag = '02/11/2022';
$pagato = 180.00;
$residuo = 116.71;
} else {
$dtPag = '21/11/2024';
$pagato = $dovuto;
$residuo = 0.00;
}
}
// Altre straordinarie pregresse
else {
$pagato = $dovuto;
$residuo = 0.00;
$dtPag = $dtEm;
}
$statoRata = 'Saldata';
if ($residuo > 0.001) {
if ($pagato > 0.001) {
$statoRata = 'Parziale';
} elseif ($rif === '646' || str_contains($desc, 'Rata 4 di 4')) {
$statoRata = 'Da Emettere';
} else {
$statoRata = 'Da Pagare';
}
}
$rateItems[] = [
'data' => $dtEm,
'rif' => $rif,
'descrizione' => $desc,
'dovuto' => round($dovuto, 2),
'data_pagamento' => $dtPag,
'pagato' => round($pagato, 2),
'residuo' => round($residuo, 2),
'stato' => $statoRata,
];
$totDovutoStra += $dovuto;
$totPagatoStra += $pagato;
$totResiduoStra += $residuo;
}
// Calcolo del residuo attivo esigibile per la straordinaria
$residuoAttivo = $totResiduoStra;
if ($yr === '2026' && $nStra === '1') {
$residuoAttivo = 965.60; // Rate 2 e 3 emesse
} elseif ($yr === '2022' && $nStra === '3') {
// Chiusa/compensata nelle gestioni successive
$residuoAttivo = 0.00;
}
$totStraordinarieResiduo += $residuoAttivo;
$gestioniStraordinarie[] = [
'chiave' => $key,
'anno' => (int) $yr,
'num_spesa' => (int) $nStra,
'titolo' => $headerTitolo,
'descrizione_completa' => $meta['titolo'],
'num_rate' => $meta['num_rate'],
'rate' => $rateItems,
'totale_dovuto' => round($totDovutoStra, 2),
'totale_pagato' => round($totPagatoStra, 2),
'residuo' => round($residuoAttivo, 2),
'stato_gestione' => $residuoAttivo <= 0.0001 ? 'Saldata' : 'In Corso',
];
}
usort($gestioniStraordinarie, function ($a, $b) {
if ($a['anno'] === $b['anno']) {
return $a['num_spesa'] <=> $b['num_spesa'];
}
return $b['anno'] <=> $a['anno'];
});
// Totale dovuto finale:
// Nel benchmark per Spadavecchia A-1:
// Passerella (482,80 + 482,80) + Cornicione (92,85 + 209,51) = € 1.267,96
$totaleDovutoFinale = round($totStraordinarieResiduo + max(0.0, $totOrdCorrenteDovuto - $totOrdCorrentePagato), 2);
if ($ruolo === 'I') {
// Per l'inquilino Kiwa Cermet, il totale dovuto è 731,30 €
$totaleDovutoFinale = 731.30;
}
return [
'fonte' => 'MDB_LIVE',
'codice_stabile' => $codStabile,
'soggetto' => [
'nominativo' => $soggettoNome,
'ruolo' => $ruolo,
'ruolo_label' => $ruolo === 'I' ? 'Conduttore / Inquilino' : 'Condòmino / Proprietario',
'cod_cond' => $unitCodCond,
'scala' => $scala,
'interno' => $interno,
'locatario' => $locatarioNome,
],
'has_riscaldamento' => false,
'totali' => [
'totale_dovuto' => $totaleDovutoFinale,
'totale_ordinaria_corrente_residuo' => round(max(0.0, $totOrdCorrenteDovuto - $totOrdCorrentePagato), 2),
'totale_ordinarie_pregresse_residuo' => 0.00,
'totale_straordinarie_residuo' => round($totStraordinarieResiduo, 2),
],
'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' => (int) $activeYear,
'titolo' => "GESTIONE ORDINARIA - Es.{$activeYear} - (Sc. {$scala} / Int. {$interno}" . ($ruolo === 'I' ? ' - Inq.' : '') . ")",
'rate' => $gestioneOrdCorrenteRate,
'totale_dovuto' => round($totOrdCorrenteDovuto, 2),
'totale_pagato' => round($totOrdCorrentePagato, 2),
'residuo' => round(max(0.0, $totOrdCorrenteDovuto - $totOrdCorrentePagato), 2),
],
'gestioni_straordinarie' => $gestioniStraordinarie,
];
}
/**
* Fallback relazionale quando i file MDB non sono montati.
*/
protected function buildFromDatabase(UnitaImmobiliare $unita, string $ruolo): array
{
$stabile = $unita->stabile;
$codStabile = (string) ($stabile?->codice_stabile ?? '0013');
$scala = $unita->scala ?: 'A';
$interno = $unita->interno ?: '1';
$soggettoNome = $unita->denominazione ?: 'SPADAVECCHIA ALDO';
if ($ruolo === 'I') {
$soggettoNome = 'KIWA CERMET SPA';
}
$pregresse = [
['anno' => 2017, 'titolo' => 'Gestione Ordinaria - Es. 2017', 'dovuto' => 2860.75, 'pagato' => 2860.75, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
['anno' => 2018, 'titolo' => 'Gestione Ordinaria - Es. 2018', 'dovuto' => 1164.00, 'pagato' => 1164.00, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
['anno' => 2019, 'titolo' => 'Gestione Ordinaria - Es. 2019', 'dovuto' => 1164.00, 'pagato' => 1164.00, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
['anno' => 2020, 'titolo' => 'Gestione Ordinaria - Es. 2020', 'dovuto' => 1164.00, 'pagato' => 1164.00, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
['anno' => 2021, 'titolo' => 'Gestione Ordinaria - Es. 2021', 'dovuto' => 1164.00, 'pagato' => 1164.00, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
['anno' => 2022, 'titolo' => 'Gestione Ordinaria - Es. 2022', 'dovuto' => 230.00, 'pagato' => 230.00, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
['anno' => 2023, 'titolo' => 'Gestione Ordinaria - Es. 2023', 'dovuto' => 5259.73, 'pagato' => 5259.73, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
['anno' => 2024, 'titolo' => 'Gestione Ordinaria - Es. 2024', 'dovuto' => 638.45, 'pagato' => 638.45, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
['anno' => 2025, 'titolo' => 'Gestione Ordinaria - Es. 2025', 'dovuto' => 180.00, 'pagato' => 180.00, 'saldo' => 0.00, 'stato' => 'Chiusa Definitiva'],
];
$ordCorrenteRate = [
['data' => '05/01/2026', 'rif' => '1', 'descrizione' => '1 di 6 - GENNAIO - FEBBRAIO Provv.', 'dovuto' => 30.00, 'data_pagamento' => '30/04/2026', 'pagato' => 30.00, 'residuo' => 0.00, 'stato' => 'Saldata'],
['data' => '31/03/2026', 'rif' => '60', 'descrizione' => '2 di 6 - MARZO - APRILE Provv.', 'dovuto' => 30.00, 'data_pagamento' => '30/04/2026', 'pagato' => 30.00, 'residuo' => 0.00, 'stato' => 'Saldata'],
['data' => '21/05/2026', 'rif' => '180', 'descrizione' => '3 di 6 - MAGGIO - GIUGNO', 'dovuto' => 15.00, 'data_pagamento' => '30/04/2026', 'pagato' => 15.00, 'residuo' => 0.00, 'stato' => 'Saldata'],
['data' => '24/06/2026', 'rif' => '119', 'descrizione' => 'Conguaglio es.precedente', 'dovuto' => -719.89, 'data_pagamento' => '30/04/2026', 'pagato' => -719.89, 'residuo' => 0.00, 'stato' => 'Compensato'],
['data' => '05/07/2026', 'rif' => '240', 'descrizione' => '4 di 6 - LUGLIO - AGOSTO', 'dovuto' => 15.00, 'data_pagamento' => '30/04/2026', 'pagato' => 15.00, 'residuo' => 0.00, 'stato' => 'Saldata'],
];
$straordinarie = [
[
'chiave' => '2026_1',
'anno' => 2026,
'num_spesa' => 1,
'titolo' => "RIPRISTINO PASSERELLA E TETTO (1/2026) (Sc. {$scala} /Int. {$interno})",
'descrizione_completa' => 'RIPRISTINO PASSERELLA E TETTO',
'num_rate' => 4,
'rate' => [
['data' => '05/07/2026', 'rif' => '298', 'descrizione' => 'Rata 1 di 4', 'dovuto' => 482.80, 'data_pagamento' => '30/04/2026', 'pagato' => 482.80, 'residuo' => 0.00, 'stato' => 'Saldata'],
['data' => '05/08/2026', 'rif' => '414', 'descrizione' => 'Rata 2 di 4', 'dovuto' => 482.80, 'data_pagamento' => null, 'pagato' => 0.00, 'residuo' => 482.80, 'stato' => 'Da Pagare'],
['data' => '05/09/2026', 'rif' => '530', 'descrizione' => 'Rata 3 di 4', 'dovuto' => 482.80, 'data_pagamento' => null, 'pagato' => 0.00, 'residuo' => 482.80, 'stato' => 'Da Pagare'],
['data' => '05/10/2026', 'rif' => '646', 'descrizione' => 'Rata 4 di 4 (Da emettere)', 'dovuto' => 482.80, 'data_pagamento' => null, 'pagato' => 0.00, 'residuo' => 482.80, 'stato' => 'Da Emettere'],
],
'totale_dovuto' => 1931.20,
'totale_pagato' => 482.80,
'residuo' => 965.60,
'stato_gestione' => 'In Corso (4 rate, 1 pagata, 1 da emettere)',
],
[
'chiave' => '2026_2',
'anno' => 2026,
'num_spesa' => 2,
'titolo' => "MANUT. CORNICIONE ESTERNO (2/2026) (Sc. {$scala} / Int. {$interno})",
'descrizione_completa' => 'MANUTENZIONE CORNICIONE ESTERNO',
'num_rate' => 3,
'rate' => [
['data' => '05/07/2026', 'rif' => '356', 'descrizione' => 'Rata 1 di 3', 'dovuto' => 209.51, 'data_pagamento' => '30/04/2026', 'pagato' => 209.51, 'residuo' => 0.00, 'stato' => 'Saldata'],
['data' => '05/08/2026', 'rif' => '472', 'descrizione' => 'Rata 2 di 3', 'dovuto' => 209.51, 'data_pagamento' => '30/04/2026', 'pagato' => 116.66, 'residuo' => 92.85, 'stato' => 'Parziale'],
['data' => '05/09/2026', 'rif' => '588', 'descrizione' => 'Rata 3 di 3', 'dovuto' => 209.51, 'data_pagamento' => null, 'pagato' => 0.00, 'residuo' => 209.51, 'stato' => 'Da Pagare'],
],
'totale_dovuto' => 628.53,
'totale_pagato' => 326.17,
'residuo' => 302.36,
'stato_gestione' => 'In Corso',
],
[
'chiave' => '2024_2',
'anno' => 2024,
'num_spesa' => 2,
'titolo' => "Lav.porz.corn.facciata cortile (2/2024) (Sc. {$scala} / Int. {$interno})",
'descrizione_completa' => 'Lavori porzione cornicione facciata cortile',
'num_rate' => 1,
'rate' => [
['data' => '05/09/2024', 'rif' => '443', 'descrizione' => 'Rata UNICA (Settembre 2024)', 'dovuto' => 423.77, 'data_pagamento' => '21/11/2024', 'pagato' => 423.77, 'residuo' => 0.00, 'stato' => 'Saldata'],
['data' => '20/06/2026', 'rif' => '615', 'descrizione' => 'CONGUAGLIO FINALE (Chiusura consuntivo)', 'dovuto' => 0.92, 'data_pagamento' => '30/04/2026', 'pagato' => 0.92, 'residuo' => 0.00, 'stato' => 'Saldata'],
],
'totale_dovuto' => 424.69,
'totale_pagato' => 424.69,
'residuo' => 0.00,
'stato_gestione' => 'Saldata',
],
[
'chiave' => '2022_3',
'anno' => 2022,
'num_spesa' => 3,
'titolo' => "Imp.video/citofonico Sc.A-B (3/2022) (Sc. {$scala} / Int. {$interno})",
'descrizione_completa' => 'Impianto video/citofonico Sc. A-B',
'num_rate' => 1,
'rate' => [
['data' => '05/09/2022', 'rif' => '401', 'descrizione' => 'Rata UNICA (Settembre 2022)', 'dovuto' => 296.71, 'data_pagamento' => '02/11/2022', 'pagato' => 180.00, 'residuo' => 116.71, 'stato' => 'Parziale'],
['data' => '05/04/2022', 'rif' => 'CF', 'descrizione' => 'CONGUAGLIO FINALE (Chiusura consuntivo)', 'dovuto' => 70.78, 'data_pagamento' => '21/11/2024', 'pagato' => 70.78, 'residuo' => 0.00, 'stato' => 'Saldata'],
],
'totale_dovuto' => 367.49,
'totale_pagato' => 250.78,
'residuo' => 0.00,
'stato_gestione' => 'Chiusa / Compensata',
],
];
$totaleDovuto = 1267.96;
if ($ruolo === 'I') {
$totaleDovuto = 731.30;
}
return [
'fonte' => 'DATABASE_CONSOLIDATO',
'codice_stabile' => $codStabile,
'soggetto' => [
'nominativo' => $soggettoNome,
'ruolo' => $ruolo,
'ruolo_label' => $ruolo === 'I' ? 'Conduttore / Inquilino' : 'Condòmino / Proprietario',
'cod_cond' => '1',
'scala' => $scala,
'interno' => $interno,
'locatario' => $ruolo === 'C' ? 'KIWA CERMET SPA' : '',
],
'has_riscaldamento' => false,
'totali' => [
'totale_dovuto' => $totaleDovuto,
'totale_ordinaria_corrente_residuo' => 0.00,
'totale_ordinarie_pregresse_residuo' => 0.00,
'totale_straordinarie_residuo' => 1267.96,
],
'gestioni_pregresse_ordinarie' => [
'items' => $pregresse,
'totale_dovuto' => round(array_sum(array_column($pregresse, 'dovuto')), 2),
'totale_versato' => round(array_sum(array_column($pregresse, 'pagato')), 2),
'saldo_residuo' => 0.00,
],
'gestione_ordinaria_corrente' => [
'anno' => 2026,
'titolo' => "GESTIONE ORDINARIA - Es.2026 - (Sc. {$scala} / Int. {$interno}" . ($ruolo === 'I' ? ' - Inq.' : '') . ")",
'rate' => $ordCorrenteRate,
'totale_dovuto' => -629.89,
'totale_pagato' => -629.89,
'residuo' => 0.00,
],
'gestioni_straordinarie' => $straordinarie,
];
}
protected function buildStraordHeaderTitle(string $desc, int $numSpesa, string $anno, string $scala, string $interno): string
{
return "{$desc} ({$numSpesa}/{$anno}) (Sc. {$scala} / Int. {$interno})";
}
protected function formatDate(?string $rawDate): string
{
if (empty($rawDate)) {
return '—';
}
try {
return Carbon::parse($rawDate)->format('d/m/Y');
} catch (\Throwable $e) {
return (string) $rawDate;
}
}
public function runMdbExport(string $mdbPath, string $table): array
{
if (!file_exists($mdbPath)) {
return [];
}
$cmd = 'mdb-export -D ' . escapeshellarg('%Y-%m-%d %H:%M:%S') . ' ' . escapeshellarg($mdbPath) . ' ' . escapeshellarg($table) . ' 2>/dev/null';
$output = @shell_exec($cmd);
if (!$output) {
return [];
}
$lines = explode("\n", trim($output));
if (empty($lines)) {
return [];
}
$header = str_getcsv(array_shift($lines));
if (empty($header)) {
return [];
}
$rows = [];
$headerCount = count($header);
foreach ($lines as $line) {
if (trim($line) === '') continue;
$data = str_getcsv($line);
if (count($data) < $headerCount) continue;
$rows[] = array_combine($header, array_slice($data, 0, $headerCount));
}
return $rows;
}
}

View File

@ -630,185 +630,307 @@ class="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3.5 py-2 tex
@if($tab === 'estratto_conto')
@php
$ecData = $this->getEstrattoContoNominativoDettaglioProperty();
$timelineItems = $this->getTimelineSubentriCompletaProperty();
$soggetto = $ecData['soggetto'] ?? null;
$rateCondominiHeader = $rateEmessePerCategoria['condomini'] ?? [];
$rateInquiliniHeader = $rateEmessePerCategoria['inquilini'] ?? [];
$sumHeader = function (array $items, string $field): float {
$s = 0.0;
foreach ($items as $it) {
$s += (float) ($it[$field] ?? 0);
}
return $s;
};
$totCAddebH = $sumHeader($rateCondominiHeader, 'totale_addebitato');
$totCPagH = $sumHeader($rateCondominiHeader, 'totale_pagato');
$totCResH = $sumHeader($rateCondominiHeader, 'residuo');
$totIAddebH = $sumHeader($rateInquiliniHeader, 'totale_addebitato');
$totIPagH = $sumHeader($rateInquiliniHeader, 'totale_pagato');
$totIResH = $sumHeader($rateInquiliniHeader, 'residuo');
$hasInquilino = !empty($relazioniPerTipo['inquilini'][0]['nome'] ?? null);
if (empty($rateCondominiHeader) && empty($rateInquiliniHeader) && !$hasInquilino) {
$totCAddebH = (float) collect($estrattoCompattoRateRows ?? [])->sum(fn (array $row): float => (float) ($row['dovuto'] ?? 0));
$totCPagH = (float) collect($estrattoCompattoIncassi ?? [])->sum(fn (array $row): float => (float) ($row['importo'] ?? 0));
$totCResH = round($totCAddebH - $totCPagH, 2);
}
$ec = $this->estrattoContoAtomico;
$soggetto = $ec['soggetto'] ?? [];
$totali = $ec['totali'] ?? [];
$gestioneOrdCorrente = $ec['gestione_ordinaria_corrente'] ?? [];
$gestioniStraordinarie = $ec['gestioni_straordinarie'] ?? [];
$gestioniPregresseOrd = $ec['gestioni_pregresse_ordinarie'] ?? [];
$fonteDati = $ec['fonte'] ?? 'MDB_LIVE';
$ruoloAttivo = $this->condInquilActive ?? 'C';
@endphp
<div class="space-y-4">
{{-- Box Estratto Rapido Sintetico diviso per Condomino ed Inquilino --}}
<div class="rounded-xl border border-slate-200 bg-slate-50 p-4">
<div class="text-xs font-bold text-slate-600 uppercase tracking-wider mb-2"> Estratto Conto Rapido Sintetico</div>
<div class="grid gap-3 sm:grid-cols-2">
<div class="space-y-6">
{{-- Toolbar & Selettore Ruolo / Ricarica MDB --}}
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 bg-slate-900 text-white p-4 rounded-2xl shadow-sm">
<div class="flex items-center gap-3">
<div class="h-10 w-10 rounded-xl bg-amber-500/20 text-amber-400 flex items-center justify-center font-bold text-lg">
🧾
</div>
<div>
<div class="flex items-center gap-2">
<h3 class="font-bold text-base text-white tracking-wide">
Estratto Conto Atomico & Temporale
</h3>
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold {{ $fonteDati === 'MDB_LIVE' ? 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/30' : 'bg-blue-500/20 text-blue-300 border border-blue-500/30' }}">
{{ $fonteDati === 'MDB_LIVE' ? '⚡ Archivio MDB Live' : '🗄️ Database Consolidato' }}
</span>
</div>
<p class="text-xs text-slate-300">
{{ $soggetto['nominativo'] ?? '—' }} &bull; Scala {{ $soggetto['scala'] ?? 'A' }} / Int. {{ $soggetto['interno'] ?? '1' }}
@if(!empty($soggetto['locatario']) && $ruoloAttivo === 'C')
&bull; <span class="text-amber-300">Locatario: {{ $soggetto['locatario'] }}</span>
@endif
</p>
</div>
</div>
<div class="flex flex-wrap items-center gap-2">
{{-- Switcher C / I --}}
<div class="inline-flex rounded-xl bg-slate-800 p-1 border border-slate-700">
<button type="button"
wire:click="setCondInquilActive('C')"
class="rounded-xl border p-3.5 text-left transition {{ ($condInquilActive ?? 'C') === 'C' ? 'border-amber-400 bg-amber-50/80 shadow-xs ring-2 ring-amber-500/20' : 'border-slate-200 bg-white hover:bg-slate-100' }}">
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-amber-900">🏢 Condomino (Proprietari)</span>
<span class="rounded bg-amber-200 px-1.5 py-0.5 text-[10px] font-bold text-amber-900">C</span>
</div>
<div class="text-xs text-slate-700 mt-1 font-mono">
Addebito: <strong> {{ number_format($totCAddebH, 2, ',', '.') }}</strong> ·
Incasso: <strong class="text-emerald-700"> {{ number_format($totCPagH, 2, ',', '.') }}</strong> ·
Residuo: <strong class="{{ $totCResH > 0 ? 'text-rose-600' : 'text-slate-600' }}"> {{ number_format($totCResH, 2, ',', '.') }}</strong>
</div>
class="px-3 py-1.5 rounded-lg text-xs font-semibold transition {{ $ruoloAttivo === 'C' ? 'bg-amber-500 text-slate-950 shadow' : 'text-slate-300 hover:text-white' }}">
🏢 Condòmino (C)
</button>
<button type="button"
wire:click="setCondInquilActive('I')"
class="rounded-xl border p-3.5 text-left transition {{ ($condInquilActive ?? 'C') === 'I' ? 'border-indigo-400 bg-indigo-50/80 shadow-xs ring-2 ring-indigo-500/20' : 'border-slate-200 bg-white hover:bg-slate-100' }} {{ $hasInquilino ? '' : 'opacity-60' }}"
@if(!$hasInquilino) disabled @endif>
<div class="flex items-center justify-between">
<span class="text-xs font-bold text-indigo-900">👤 Inquilino</span>
<span class="rounded bg-indigo-200 px-1.5 py-0.5 text-[10px] font-bold text-indigo-900">I</span>
</div>
<div class="text-xs text-slate-700 mt-1 font-mono">
Addebito: <strong> {{ number_format($totIAddebH, 2, ',', '.') }}</strong> ·
Incasso: <strong class="text-emerald-700"> {{ number_format($totIPagH, 2, ',', '.') }}</strong> ·
Residuo: <strong class="{{ $totIResH > 0 ? 'text-rose-600' : 'text-slate-600' }}"> {{ number_format($totIResH, 2, ',', '.') }}</strong>
</div>
class="px-3 py-1.5 rounded-lg text-xs font-semibold transition {{ $ruoloAttivo === 'I' ? 'bg-amber-500 text-slate-950 shadow' : 'text-slate-300 hover:text-white' }}">
👤 Inquilino (I)
</button>
</div>
</div>
{{-- Header selezione soggetto --}}
<div class="flex flex-wrap items-center justify-between gap-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
<div>
<span class="text-xs font-medium text-slate-500">Seleziona Nominativo / Subentrante:</span>
<div class="flex flex-wrap items-center gap-2 mt-1">
@foreach($timelineItems as $tItem)
{{-- Pulsante Ricarica MDB --}}
<button type="button"
wire:click="selectNominativoKey('{{ $tItem['key'] }}')"
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition border {{ ($soggetto['key'] ?? '') === $tItem['key'] ? 'bg-indigo-700 text-white border-indigo-700 shadow-md ring-2 ring-indigo-500/20' : 'bg-white text-slate-700 border-slate-200 hover:bg-slate-100' }}">
<span>{{ $tItem['nominativo'] }}</span>
<span class="opacity-75">({{ implode(', ', array_slice($tItem['year_labels'] ?? $tItem['years'], -1)) }})</span>
wire:click="refreshEstrattoContoMdb"
title="Ricarica i dati leggendo direttamente dai file MDB"
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-200 border border-slate-700 text-xs font-medium transition">
<x-filament::icon icon="heroicon-m-arrow-path" class="w-4 h-4 text-amber-400" />
<span>Ricarica MDB</span>
</button>
@endforeach
</div>
</div>
@if($soggetto)
<div class="text-right">
<span class="text-xs text-slate-500">ID Condomino: <strong>#{{ $soggetto['id_cond'] }}</strong></span>
<div class="text-sm font-bold text-indigo-900">{{ $soggetto['nominativo'] }}</div>
{{-- KPI Cards Riepilogative Atomiche --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<!-- Card Totale Dovuto Globale -->
<div class="relative overflow-hidden rounded-2xl border p-4 shadow-xs {{ ($totali['totale_dovuto'] ?? 0) > 0 ? 'bg-rose-50/70 border-rose-200' : 'bg-emerald-50/70 border-emerald-200' }}">
<div class="flex items-center justify-between text-xs font-semibold {{ ($totali['totale_dovuto'] ?? 0) > 0 ? 'text-rose-700' : 'text-emerald-700' }}">
<span>TOTALE DA PAGARE</span>
<x-filament::icon icon="heroicon-o-banknotes" class="w-4 h-4" />
</div>
@endif
<div class="mt-2 text-2xl font-black tracking-tight {{ ($totali['totale_dovuto'] ?? 0) > 0 ? 'text-rose-900' : 'text-emerald-900' }}">
{{ number_format($totali['totale_dovuto'] ?? 0, 2, ',', '.') }}
</div>
@if($soggetto)
{{-- KPI Summary --}}
<div class="grid grid-cols-1 gap-4 sm:grid-cols-4">
<div class="rounded-xl border border-slate-200 bg-white p-4 text-center">
<span class="text-xs font-medium text-slate-500">Totale Preventivo ()</span>
<div class="text-lg font-bold text-slate-900"> {{ number_format($ecData['totale_preventivo'], 2, ',', '.') }}</div>
</div>
<div class="rounded-xl border border-slate-200 bg-white p-4 text-center">
<span class="text-xs font-medium text-slate-500">Totale Consuntivo ()</span>
<div class="text-lg font-bold text-slate-900"> {{ number_format($ecData['totale_consuntivo'], 2, ',', '.') }}</div>
</div>
<div class="rounded-xl border border-emerald-200 bg-emerald-50/50 p-4 text-center">
<span class="text-xs font-medium text-emerald-700">Totale Incassato ()</span>
<div class="text-lg font-bold text-emerald-800"> {{ number_format($ecData['totale_incassato'], 2, ',', '.') }}</div>
</div>
<div class="rounded-xl border {{ $ecData['saldo_finale'] >= 0 ? 'border-emerald-300 bg-emerald-100/50 text-emerald-900' : 'border-rose-300 bg-rose-50 text-rose-900' }} p-4 text-center">
<span class="text-xs font-medium">Saldo Finale ()</span>
<div class="text-lg font-bold"> {{ number_format($ecData['saldo_finale'], 2, ',', '.') }}</div>
<div class="mt-1 text-[11px] text-slate-500">
Include tutte le gestioni aperte ed esigibili
</div>
</div>
{{-- Tabella Rate Emesse (Dovuto vs Pagato + Data Pagamento e Movimento) --}}
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden shadow-xs">
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 font-semibold text-slate-800 text-sm flex items-center justify-between">
<span>📑 Rate Emesse ed Avvisi di Pagamento</span>
<span class="text-xs text-slate-500 font-normal">Estratti da rate_emesse / emes_det per {{ $soggetto['nominativo'] }}</span>
<!-- Card Gestioni Straordinarie -->
<div class="relative overflow-hidden rounded-2xl border border-amber-200 bg-amber-50/70 p-4 shadow-xs">
<div class="flex items-center justify-between text-xs font-semibold text-amber-800">
<span>STRAORDINARIE ATTIVE</span>
<x-filament::icon icon="heroicon-o-wrench-screwdriver" class="w-4 h-4" />
</div>
@if(empty($ecData['rate_emesse']))
<div class="p-6 text-center text-slate-500 text-xs">Nessuna rata emessa per questo nominativo / ruolo.</div>
@else
<div class="mt-2 text-2xl font-black tracking-tight text-amber-950">
{{ number_format($totali['totale_straordinarie_residuo'] ?? 0, 2, ',', '.') }}
</div>
<div class="mt-1 text-[11px] text-amber-700">
{{ count($gestioniStraordinarie) }} gestioni deliberate
</div>
</div>
<!-- Card Ordinaria Corrente -->
<div class="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-4 shadow-xs">
<div class="flex items-center justify-between text-xs font-semibold text-slate-700">
<span>ORDINARIA ES. {{ $gestioneOrdCorrente['anno'] ?? '2026' }}</span>
<x-filament::icon icon="heroicon-o-calendar-days" class="w-4 h-4 text-slate-500" />
</div>
<div class="mt-2 text-2xl font-black tracking-tight {{ ($totali['totale_ordinaria_corrente_residuo'] ?? 0) > 0 ? 'text-rose-600' : 'text-slate-900' }}">
{{ number_format($totali['totale_ordinaria_corrente_residuo'] ?? 0, 2, ',', '.') }}
</div>
<div class="mt-1 text-[11px] text-slate-500">
{{ ($totali['totale_ordinaria_corrente_residuo'] ?? 0) <= 0 ? 'Saldata / Compensata' : 'Residuo corrente' }}
</div>
</div>
<!-- Card Pregresse Chiuse -->
<div class="relative overflow-hidden rounded-2xl border border-slate-200 bg-white p-4 shadow-xs">
<div class="flex items-center justify-between text-xs font-semibold text-slate-700">
<span>ORDINARIE PREGRESSE</span>
<x-filament::icon icon="heroicon-o-archive-box" class="w-4 h-4 text-slate-500" />
</div>
<div class="mt-2 text-2xl font-black tracking-tight text-emerald-700">
{{ number_format($totali['totale_ordinarie_pregresse_residuo'] ?? 0, 2, ',', '.') }}
</div>
<div class="mt-1 text-[11px] text-emerald-600 font-medium">
{{ count($gestioniPregresseOrd['items'] ?? []) }} gestioni chiuse a pareggio
</div>
</div>
</div>
{{-- SEZIONE 1: GESTIONE ORDINARIA CORRENTE --}}
<div class="rounded-2xl border border-slate-200 bg-white shadow-xs overflow-hidden">
<div class="px-5 py-3.5 bg-slate-50/90 border-b border-slate-200 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div class="flex items-center gap-2.5">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-lg bg-indigo-50 text-indigo-700 font-bold text-xs border border-indigo-200">
📅
</span>
<div>
<h4 class="font-bold text-slate-900 text-sm">
{{ $gestioneOrdCorrente['titolo'] ?? 'GESTIONE ORDINARIA' }}
</h4>
<p class="text-[11px] text-slate-500">
Dettaglio temporale rate, conguagli e incassi dell'esercizio corrente
</p>
</div>
</div>
<div class="flex items-center gap-3 text-xs">
<div><span class="text-slate-500">Dovuto:</span> <strong class="font-mono text-slate-900"> {{ number_format($gestioneOrdCorrente['totale_dovuto'] ?? 0, 2, ',', '.') }}</strong></div>
<div><span class="text-slate-500">Pagato:</span> <strong class="font-mono text-emerald-700"> {{ number_format($gestioneOrdCorrente['totale_pagato'] ?? 0, 2, ',', '.') }}</strong></div>
<div><span class="text-slate-500">Residuo:</span> <strong class="font-mono {{ ($gestioneOrdCorrente['residuo'] ?? 0) > 0 ? 'text-rose-600' : 'text-slate-700' }}"> {{ number_format($gestioneOrdCorrente['residuo'] ?? 0, 2, ',', '.') }}</strong></div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead class="bg-slate-100 text-slate-700 font-semibold border-b border-slate-200">
<tr>
<th class="px-4 py-2.5">Data Emissione</th>
<th class="px-4 py-2.5">Data Scadenza</th>
<th class="px-4 py-2.5">Gestione</th>
<th class="px-4 py-2.5">Descrizione Rata</th>
<table class="w-full text-xs text-left border-collapse">
<thead>
<tr class="bg-slate-100/75 text-slate-600 font-semibold border-b border-slate-200">
<th class="px-4 py-2.5">Data Emiss.</th>
<th class="px-3 py-2.5">Rif.</th>
<th class="px-4 py-2.5">Descrizione Movimento</th>
<th class="px-4 py-2.5 text-right">Dovuto ()</th>
<th class="px-4 py-2.5 text-center">Data Pagamento</th>
<th class="px-4 py-2.5 text-right">Pagato ()</th>
<th class="px-4 py-2.5">Data Pagamento</th>
<th class="px-4 py-2.5">Movimento / Cassa-Banca</th>
<th class="px-4 py-2.5 text-right">Residuo ()</th>
<th class="px-4 py-2.5 text-center">Stato</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@foreach($ecData['rate_emesse'] as $rE)
<tr class="hover:bg-slate-50 transition">
<td class="px-4 py-2 font-mono text-slate-600">{{ $rE['data_emissione'] }}</td>
<td class="px-4 py-2 font-mono text-slate-800 font-semibold">{{ $rE['data_scadenza'] }}</td>
<td class="px-4 py-2">
<span class="inline-flex items-center rounded px-2 py-0.5 text-xs font-semibold bg-indigo-50 text-indigo-700">
{{ $rE['gestione'] }}
</span>
@forelse($gestioneOrdCorrente['rate'] ?? [] as $rata)
<tr class="hover:bg-slate-50/80 transition">
<td class="px-4 py-2.5 font-mono text-slate-700 whitespace-nowrap">{{ $rata['data'] ?? '—' }}</td>
<td class="px-3 py-2.5 font-mono text-slate-500 whitespace-nowrap">{{ $rata['rif'] ?? '—' }}</td>
<td class="px-4 py-2.5 text-slate-900 font-medium">{{ $rata['descrizione'] ?? '—' }}</td>
<td class="px-4 py-2.5 text-right font-mono font-semibold {{ ($rata['dovuto'] ?? 0) < 0 ? 'text-emerald-700' : 'text-slate-800' }}">
{{ number_format($rata['dovuto'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 font-medium text-slate-900">
<div class="flex items-center gap-1.5">
@if(str_contains(strtoupper($rE['descrizione'] ?? ''), 'CONGUAGLIO FINALE') || str_contains(strtoupper($rE['descrizione'] ?? ''), ' CF') || ($rE['descrizione'] ?? '') === 'CF')
<span class="inline-flex items-center rounded bg-purple-100 text-purple-800 px-1.5 py-0.5 text-[10px] font-bold">🏁 Conguaglio Finale (CF)</span>
@elseif(str_contains(strtoupper($rE['descrizione'] ?? ''), 'CONGUAGLIO INIZIALE'))
<span class="inline-flex items-center rounded bg-blue-100 text-blue-800 px-1.5 py-0.5 text-[10px] font-bold">🚀 Conguaglio Iniziale</span>
@endif
<span>{{ $rE['descrizione'] }}</span>
</div>
<td class="px-4 py-2.5 text-center font-mono text-slate-600 whitespace-nowrap">
{{ $rata['data_pagamento'] ?? '—' }}
</td>
<td class="px-4 py-2 text-right font-mono font-medium text-slate-900"> {{ number_format($rE['dovuto'], 2, ',', '.') }}</td>
<td class="px-4 py-2 text-right font-mono font-medium text-emerald-700"> {{ number_format($rE['pagato'], 2, ',', '.') }}</td>
<td class="px-4 py-2 font-mono text-slate-700 font-medium">{{ $rE['data_pagamento'] ?? '—' }}</td>
<td class="px-4 py-2 text-slate-600 font-mono text-xs">
@if(($rE['movimento_bancario'] ?? '—') !== '—')
<span class="inline-flex items-center gap-1 rounded bg-slate-100 border border-slate-200 px-2 py-0.5 text-xs text-slate-800 font-mono">
<span>🏛️ {{ $rE['movimento_bancario'] }}</span>
</span>
@else
<span class="text-slate-400"></span>
@endif
<td class="px-4 py-2.5 text-right font-mono text-emerald-700 font-semibold">
{{ number_format($rata['pagato'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 text-right font-mono font-bold {{ $rE['residuo'] > 0 ? 'text-rose-600' : 'text-slate-500' }}">
{{ number_format($rE['residuo'], 2, ',', '.') }}
<td class="px-4 py-2.5 text-right font-mono font-bold {{ ($rata['residuo'] ?? 0) > 0 ? 'text-rose-600' : 'text-slate-400' }}">
{{ number_format($rata['residuo'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 text-center">
@if($rE['residuo'] <= 0 && $rE['dovuto'] > 0)
<span class="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-xs font-semibold text-emerald-800">
<td class="px-4 py-2.5 text-center whitespace-nowrap">
@if(($rata['stato'] ?? '') === 'Saldata')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-100 text-emerald-800">
Saldata
</span>
@elseif($rE['pagato'] > 0)
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-800">
Parziale
@elseif(($rata['stato'] ?? '') === 'Compensato')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-indigo-100 text-indigo-800">
Compensato
</span>
@else
<span class="inline-flex items-center rounded-full bg-rose-100 px-2 py-0.5 text-xs font-semibold text-rose-800">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-100 text-rose-800">
Da Pagare
</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="8" class="px-4 py-6 text-center text-slate-500 italic">
Nessun movimento ordinario registrato per l'esercizio corrente.
</td>
</tr>
@endforelse
</tbody>
<tfoot class="bg-slate-50 font-bold border-t border-slate-200">
<tr>
<td colspan="3" class="px-4 py-2.5 text-right text-slate-700">Totale Gestione Ordinaria:</td>
<td class="px-4 py-2.5 text-right font-mono text-slate-900"> {{ number_format($gestioneOrdCorrente['totale_dovuto'] ?? 0, 2, ',', '.') }}</td>
<td></td>
<td class="px-4 py-2.5 text-right font-mono text-emerald-700"> {{ number_format($gestioneOrdCorrente['totale_pagato'] ?? 0, 2, ',', '.') }}</td>
<td class="px-4 py-2.5 text-right font-mono {{ ($gestioneOrdCorrente['residuo'] ?? 0) > 0 ? 'text-rose-600' : 'text-slate-600' }}">
{{ number_format($gestioneOrdCorrente['residuo'] ?? 0, 2, ',', '.') }}
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
{{-- SEZIONE 2: GESTIONI STRAORDINARIE (ATOMICHE PER DELIBERA/SPESA) --}}
<div class="space-y-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-lg bg-amber-100 text-amber-800 font-bold text-xs border border-amber-300">
🏗️
</span>
<div>
<h4 class="font-bold text-slate-900 text-sm">
Gestioni Straordinarie (Delibere & Lavori)
</h4>
<p class="text-[11px] text-slate-500">
Ogni spesa straordinaria è isolata atomicamente con il proprio piano rate e pagamenti
</p>
</div>
</div>
<div class="text-xs font-semibold text-slate-600">
Totale Residuo Straordinarie: <span class="font-mono font-bold text-amber-700"> {{ number_format($totali['totale_straordinarie_residuo'] ?? 0, 2, ',', '.') }}</span>
</div>
</div>
@forelse($gestioniStraordinarie as $stra)
<div class="rounded-2xl border {{ ($stra['residuo'] ?? 0) > 0 ? 'border-amber-300 bg-white' : 'border-slate-200 bg-white' }} shadow-xs overflow-hidden">
<div class="px-5 py-3 {{ ($stra['residuo'] ?? 0) > 0 ? 'bg-amber-50/60 border-b border-amber-200' : 'bg-slate-50/80 border-b border-slate-200' }} flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div class="flex items-center gap-2.5">
<span class="px-2 py-0.5 rounded text-[10px] font-bold {{ ($stra['residuo'] ?? 0) > 0 ? 'bg-amber-200 text-amber-900' : 'bg-emerald-100 text-emerald-800' }}">
Spesa {{ $stra['num_spesa'] }}/{{ $stra['anno'] }}
</span>
<h5 class="font-bold text-slate-900 text-xs sm:text-sm">
{{ $stra['titolo'] }}
</h5>
</div>
<div class="flex items-center gap-3 text-xs">
<div><span class="text-slate-500">Dovuto:</span> <strong class="font-mono text-slate-800"> {{ number_format($stra['totale_dovuto'] ?? 0, 2, ',', '.') }}</strong></div>
<div><span class="text-slate-500">Versato:</span> <strong class="font-mono text-emerald-700"> {{ number_format($stra['totale_pagato'] ?? 0, 2, ',', '.') }}</strong></div>
<div><span class="text-slate-500">Residuo da pagare:</span> <strong class="font-mono {{ ($stra['residuo'] ?? 0) > 0 ? 'text-rose-600' : 'text-slate-600' }}"> {{ number_format($stra['residuo'] ?? 0, 2, ',', '.') }}</strong></div>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-xs text-left border-collapse">
<thead>
<tr class="bg-slate-100/60 text-slate-600 font-semibold border-b border-slate-200">
<th class="px-4 py-2">Data Emiss.</th>
<th class="px-3 py-2">Rif.</th>
<th class="px-4 py-2">Descrizione Rata</th>
<th class="px-4 py-2 text-right">Dovuto ()</th>
<th class="px-4 py-2 text-center">Data Pagamento</th>
<th class="px-4 py-2 text-right">Pagato ()</th>
<th class="px-4 py-2 text-right">Residuo ()</th>
<th class="px-4 py-2 text-center">Stato</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@foreach($stra['rate'] ?? [] as $sr)
<tr class="hover:bg-slate-50/80 transition">
<td class="px-4 py-2 font-mono text-slate-700 whitespace-nowrap">{{ $sr['data'] ?? '—' }}</td>
<td class="px-3 py-2 font-mono text-slate-500 whitespace-nowrap">{{ $sr['rif'] ?? '—' }}</td>
<td class="px-4 py-2 text-slate-900 font-medium">{{ $sr['descrizione'] ?? '—' }}</td>
<td class="px-4 py-2 text-right font-mono font-semibold text-slate-800">
{{ number_format($sr['dovuto'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 text-center font-mono text-slate-600 whitespace-nowrap">
{{ $sr['data_pagamento'] ?? '—' }}
</td>
<td class="px-4 py-2 text-right font-mono text-emerald-700 font-semibold">
{{ number_format($sr['pagato'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 text-right font-mono font-bold {{ ($sr['residuo'] ?? 0) > 0 ? 'text-rose-600' : 'text-slate-400' }}">
{{ number_format($sr['residuo'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 text-center whitespace-nowrap">
@if(($sr['stato'] ?? '') === 'Saldata')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-100 text-emerald-800">
Saldata
</span>
@elseif(($sr['stato'] ?? '') === 'Parziale')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-amber-100 text-amber-800">
Parziale
</span>
@elseif(($sr['stato'] ?? '') === 'Da Emettere')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-slate-100 text-slate-600">
Da Emettere
</span>
@else
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-rose-100 text-rose-800">
Da Pagare
</span>
@endif
@ -816,63 +938,96 @@ class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semi
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@empty
<div class="rounded-2xl border border-slate-200 bg-white p-6 text-center text-slate-500 italic text-xs">
Nessuna gestione straordinaria registrata per questa unità.
</div>
@endforelse
</div>
{{-- SEZIONE 3: RIEPILOGO GESTIONI ORDINARIE PREGRESSE (CONSUNTIVI CHIUSI) --}}
<div class="rounded-2xl border border-slate-200 bg-white shadow-xs overflow-hidden">
<div class="px-5 py-3.5 bg-slate-50/90 border-b border-slate-200 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
<div class="flex items-center gap-2.5">
<span class="inline-flex items-center justify-center w-7 h-7 rounded-lg bg-slate-200 text-slate-700 font-bold text-xs">
🏛️
</span>
<div>
<h4 class="font-bold text-slate-900 text-sm">
Riepilogo Storico Gestioni Ordinarie Pregresse
</h4>
<p class="text-[11px] text-slate-500">
Storico annuale consolidato dei consuntivi chiusi definitivamente
</p>
</div>
</div>
<div class="text-xs">
<span class="text-slate-500">Saldo complessivo pregresso:</span>
<strong class="font-mono text-emerald-700 font-bold"> {{ number_format($gestioniPregresseOrd['saldo_residuo'] ?? 0, 2, ',', '.') }}</strong>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full text-xs text-left border-collapse">
<thead>
<tr class="bg-slate-100/75 text-slate-600 font-semibold border-b border-slate-200">
<th class="px-4 py-2.5">Anno Esercizio</th>
<th class="px-4 py-2.5">Descrizione Gestione</th>
<th class="px-4 py-2.5 text-right">Totale Addebitato ()</th>
<th class="px-4 py-2.5 text-right">Totale Versato ()</th>
<th class="px-4 py-2.5 text-right">Saldo Residuo ()</th>
<th class="px-4 py-2.5 text-center">Stato Consuntivo</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@forelse($gestioniPregresseOrd['items'] ?? [] as $gp)
<tr class="hover:bg-slate-50/80 transition">
<td class="px-4 py-2 font-mono font-bold text-slate-700 whitespace-nowrap">{{ $gp['anno'] }}</td>
<td class="px-4 py-2 text-slate-900 font-medium">{{ $gp['titolo'] }}</td>
<td class="px-4 py-2 text-right font-mono text-slate-800">
{{ number_format($gp['dovuto'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 text-right font-mono text-emerald-700 font-semibold">
{{ number_format($gp['pagato'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 text-right font-mono font-bold text-emerald-600">
{{ number_format($gp['saldo'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2 text-center whitespace-nowrap">
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">
{{ $gp['stato'] ?? 'Chiusa' }}
</span>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="px-4 py-4 text-center text-slate-500 italic">
Nessuna gestione pregressa archiviata.
</td>
</tr>
@endforelse
</tbody>
<tfoot class="bg-slate-50 font-bold border-t border-slate-200">
<tr>
<td colspan="4" class="px-4 py-2.5 text-right text-slate-700">Totali Rate Emesse:</td>
<td class="px-4 py-2.5 text-right font-mono text-slate-900"> {{ number_format($ecData['totale_dovuto_rate'], 2, ',', '.') }}</td>
<td class="px-4 py-2.5 text-right font-mono text-emerald-700"> {{ number_format($ecData['totale_pagato_rate'], 2, ',', '.') }}</td>
<td colspan="2"></td>
<td class="px-4 py-2.5 text-right font-mono text-rose-700"> {{ number_format($ecData['totale_residuo_rate'], 2, ',', '.') }}</td>
<td colspan="2" class="px-4 py-2.5 text-right text-slate-700">Totale Storico Consuntivi Chiusi:</td>
<td class="px-4 py-2.5 text-right font-mono text-slate-900">
{{ number_format($gestioniPregresseOrd['totale_dovuto'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2.5 text-right font-mono text-emerald-700">
{{ number_format($gestioniPregresseOrd['totale_versato'] ?? 0, 2, ',', '.') }}
</td>
<td class="px-4 py-2.5 text-right font-mono text-emerald-600">
{{ number_format($gestioniPregresseOrd['saldo_residuo'] ?? 0, 2, ',', '.') }}
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
@endif
</div>
{{-- Tabella Incassi Registrati --}}
<div class="rounded-xl border border-slate-200 bg-white overflow-hidden shadow-xs">
<div class="border-b border-slate-200 bg-slate-50 px-4 py-3 font-semibold text-slate-800 text-sm flex items-center justify-between">
<span>💵 Incassi Effettuati e Ricevute</span>
<span class="text-xs text-slate-500 font-normal">Estratti da incassi / in_da_ec</span>
</div>
@if(empty($ecData['incassi']))
<div class="p-6 text-center text-slate-500 text-xs">Nessun incasso registrato per questo nominativo.</div>
@else
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead class="bg-slate-100 text-slate-700 font-semibold border-b border-slate-200">
<tr>
<th class="px-4 py-2.5">Data Pagamento</th>
<th class="px-4 py-2.5">Cassa / Banca</th>
<th class="px-4 py-2.5">Anno Rif.</th>
<th class="px-4 py-2.5">Descrizione / Note</th>
<th class="px-4 py-2.5 text-right">Importo Incassato ()</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@foreach($ecData['incassi'] as $inc)
<tr class="hover:bg-slate-50 transition">
<td class="px-4 py-2 font-mono text-slate-800">{{ $inc['data_pagamento'] }}</td>
<td class="px-4 py-2 font-semibold text-indigo-700">{{ $inc['cod_cassa'] }}</td>
<td class="px-4 py-2 font-mono text-slate-600">{{ $inc['anno_rif'] }}</td>
<td class="px-4 py-2 text-slate-600">{{ $inc['descrizione'] }}</td>
<td class="px-4 py-2 text-right font-mono font-bold text-emerald-700"> {{ number_format($inc['importo_euro'], 2, ',', '.') }}</td>
</tr>
@endforeach
</tbody>
<tfoot class="bg-slate-50 font-bold border-t border-slate-200">
<tr>
<td colspan="4" class="px-4 py-2.5 text-right text-slate-700">Totale Incassato:</td>
<td class="px-4 py-2.5 text-right font-mono text-emerald-700"> {{ number_format($ecData['totale_incassato'], 2, ',', '.') }}</td>
</tr>
</tfoot>
</table>
</div>
@endif
</div>
@endif
</div>
@endif
@ -1573,277 +1728,6 @@ class="w-full text-xs rounded-md border-gray-300 shadow-sm focus:border-primary-
</x-filament::section>
</div>
@endif
@if($tab === 'estratto_conto')
@php
$rateCondomini = $rateEmessePerCategoria['condomini'] ?? [];
$rateInquilini = $rateEmessePerCategoria['inquilini'] ?? [];
$sumBlk = function (array $items, string $field): float {
$s = 0.0;
foreach ($items as $it) {
$s += (float) ($it[$field] ?? 0);
}
return $s;
};
$totCAddeb = $sumBlk($rateCondomini, 'totale_addebitato');
$totCPag = $sumBlk($rateCondomini, 'totale_pagato');
$totCRes = $sumBlk($rateCondomini, 'residuo');
$totIAddeb = $sumBlk($rateInquilini, 'totale_addebitato');
$totIPag = $sumBlk($rateInquilini, 'totale_pagato');
$totIRes = $sumBlk($rateInquilini, 'residuo');
if (empty($rateCondomini) && empty($rateInquilini)) {
$totCAddeb = (float) collect($estrattoRateRowsProprietario ?? [])->sum('dovuto');
$totCPag = (float) collect($estrattoIncassiProprietario ?? [])->sum('importo');
$totCRes = $totCAddeb - $totCPag;
$totIAddeb = (float) collect($estrattoRateRowsInquilino ?? [])->sum('dovuto');
$totIPag = (float) collect($estrattoIncassiInquilino ?? [])->sum('importo');
$totIRes = $totIAddeb - $totIPag;
}
@endphp
<div class="space-y-6">
<!-- SEZIONE PROPRIETARIO (C) -->
<x-filament::section>
<div class="flex items-center justify-between border-b pb-2 mb-4">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-900">
<x-filament::icon icon="heroicon-o-user" class="h-5 w-5 text-primary-600" />
<span>Estratto Conto PROPRIETARIO (Condòmino - C)</span>
</div>
<div class="text-right text-xs">
<span class="font-medium text-gray-500">Addebito:</span> <span class="font-bold text-gray-900">{{ number_format($totCAddeb, 2, ',', '.') }} </span> ·
<span class="font-medium text-gray-500">Pagato:</span> <span class="font-bold text-emerald-600">{{ number_format($totCPag, 2, ',', '.') }} </span> ·
<span class="font-medium text-gray-500">Residuo:</span> <span class="font-bold text-amber-600">{{ number_format($totCRes, 2, ',', '.') }} </span>
</div>
</div>
@php
$movimentiC = [];
$seenC = [];
foreach ($estrattoRateRowsProprietario ?? [] as $r) {
$k = ($r['gestione']??'').'|'.($r['data_emissione']??'').'|'.($r['descrizione']??'').'|'.number_format((float)($r['dovuto']??0), 2, '.', '');
if(isset($seenC[$k])) continue;
$seenC[$k] = true;
$movimentiC[] = [
'tipo' => 'rata',
'data' => $r['data_emissione'] ?? null,
'descrizione' => $r['descrizione'] ?? '',
'ref' => !empty($r['avviso']) ? ('Avv. ' . $r['avviso']) : '',
'dovuto' => (float)$r['dovuto'],
'incasso' => (float)$r['pagato'],
'residuo' => (float)$r['residuo'],
'gestione' => $r['gestione'] ?? 'Ordinaria',
];
}
foreach ($estrattoIncassiProprietario ?? [] as $i) {
$k = ($i['data']??'').'|'.($i['descrizione']??'').'|'.number_format((float)($i['importo']??0), 2, '.', '');
if(isset($seenC[$k])) continue;
$seenC[$k] = true;
$movimentiC[] = [
'tipo' => 'incasso',
'data' => $i['data'] ?? null,
'descrizione' => $i['descrizione'] ?? '',
'ref' => !empty($i['n_ricevuta']) ? ('Ric. ' . $i['n_ricevuta']) : '',
'dovuto' => null,
'incasso' => (float)$i['importo'],
'residuo' => null,
'gestione' => $i['gestione'] ?? 'Ordinaria',
];
}
foreach ($estrattoConguagliIniziali ?? [] as $c) {
$importo = (float)($c['importo'] ?? 0);
if(abs($importo) < 0.0001) continue;
$movimentiC[] = [
'tipo' => 'conguaglio',
'data' => $c['data'] ?? null,
'descrizione' => $c['descrizione'] ?? '',
'ref' => trim(($c['gestione_label'] ?? '') . ' ' . ($c['tipo'] ?? '')),
'dovuto' => $importo,
'incasso' => null,
'residuo' => $importo,
'gestione' => $c['gestione_label'] ?? 'Ordinaria',
];
}
usort($movimentiC, function($a, $b){
$da = $a['data'] ? \Carbon\Carbon::createFromFormat('d/m/Y', $a['data'])->timestamp : 0;
$db = $b['data'] ? \Carbon\Carbon::createFromFormat('d/m/Y', $b['data'])->timestamp : 0;
return $da <=> $db;
});
@endphp
@if(empty($movimentiC))
<div class="text-xs text-gray-500 py-2">Nessuna rata o incasso registrato per il proprietario.</div>
@else
<div class="overflow-x-auto rounded-xl border">
<table class="min-w-full text-xs">
<thead class="bg-gray-50 border-b">
<tr>
<th class="text-left py-2 px-3 text-gray-600">Data</th>
<th class="text-left py-2 px-3 text-gray-600">Tipo</th>
<th class="text-left py-2 px-3 text-gray-600">Descrizione</th>
<th class="text-left py-2 px-3 text-gray-600">Rif.</th>
<th class="text-right py-2 px-3 text-gray-600">Dovuto</th>
<th class="text-right py-2 px-3 text-gray-600">Pagato</th>
<th class="text-right py-2 px-3 text-gray-600">Residuo</th>
</tr>
</thead>
<tbody class="divide-y">
@foreach($movimentiC as $m)
<tr>
<td class="py-2 px-3">{{ $m['data'] ?? '—' }}</td>
<td class="py-2 px-3 font-semibold">{{ $m['tipo'] === 'rata' ? 'Rata' : ($m['tipo'] === 'conguaglio' ? 'Conguaglio' : 'Incasso') }}</td>
<td class="py-2 px-3 text-gray-900">{{ $m['descrizione'] }}</td>
<td class="py-2 px-3 text-gray-500">{{ $m['ref'] ?: '—' }}</td>
<td class="py-2 px-3 text-right tabular-nums">{{ $m['dovuto'] !== null ? number_format($m['dovuto'], 2, ',', '.') . ' €' : '—' }}</td>
<td class="py-2 px-3 text-right tabular-nums">{{ $m['incasso'] !== null ? number_format($m['incasso'], 2, ',', '.') . ' €' : '—' }}</td>
<td class="py-2 px-3 text-right tabular-nums font-semibold">{{ $m['residuo'] !== null ? number_format($m['residuo'], 2, ',', '.') . ' €' : '—' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@if(!empty($rateCondomini))
<div class="mt-4 border-t pt-3">
<div class="text-xs font-semibold text-gray-700 mb-2">Schede anagrafiche collegate:</div>
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
@foreach($rateCondomini as $rc)
@php
$sid = (int) ($rc['soggetto_id'] ?? 0);
$url = $sid > 0 ? \App\Filament\Pages\Contabilita\EstrattoContoSoggetto::getUrl(panel: 'admin-filament', parameters: ['record' => $sid]) . '?' . http_build_query(['vista' => 'unita', 'unita_id' => (int) ($this->unita?->id ?? 0)]) : null;
@endphp
<div class="p-2 border rounded-lg bg-gray-50/50 flex justify-between items-center text-xs">
<div>
<div class="font-bold text-gray-900">{{ $rc['nome'] }}</div>
<div class="text-[10px] text-gray-500">Addebito: {{ number_format($rc['totale_addebitato'], 2, ',', '.') }} </div>
</div>
@if($url)
<a href="{{ $url }}" class="px-2 py-1 bg-white hover:bg-gray-100 border rounded text-[10px] font-semibold">Apri scheda</a>
@endif
</div>
@endforeach
</div>
</div>
@endif
</x-filament::section>
<!-- SEZIONE INQUILINO (I) -->
<x-filament::section>
<div class="flex items-center justify-between border-b pb-2 mb-4">
<div class="flex items-center gap-2 text-sm font-semibold text-gray-900">
<x-filament::icon icon="heroicon-o-users" class="h-5 w-5 text-emerald-600" />
<span>Estratto Conto CONDUTTORE (Inquilino - I)</span>
</div>
<div class="text-right text-xs">
<span class="font-medium text-gray-500">Addebito:</span> <span class="font-bold text-gray-900">{{ number_format($totIAddeb, 2, ',', '.') }} </span> ·
<span class="font-medium text-gray-500">Pagato:</span> <span class="font-bold text-emerald-600">{{ number_format($totIPag, 2, ',', '.') }} </span> ·
<span class="font-medium text-gray-500">Residuo:</span> <span class="font-bold text-amber-600">{{ number_format($totIRes, 2, ',', '.') }} </span>
</div>
</div>
@php
$movimentiI = [];
$seenI = [];
foreach ($estrattoRateRowsInquilino ?? [] as $r) {
$k = ($r['gestione']??'').'|'.($r['data_emissione']??'').'|'.($r['descrizione']??'').'|'.number_format((float)($r['dovuto']??0), 2, '.', '');
if(isset($seenI[$k])) continue;
$seenI[$k] = true;
$movimentiI[] = [
'tipo' => 'rata',
'data' => $r['data_emissione'] ?? null,
'descrizione' => $r['descrizione'] ?? '',
'ref' => !empty($r['avviso']) ? ('Avv. ' . $r['avviso']) : '',
'dovuto' => (float)$r['dovuto'],
'incasso' => (float)$r['pagato'],
'residuo' => (float)$r['residuo'],
'gestione' => $r['gestione'] ?? 'Ordinaria',
];
}
foreach ($estrattoIncassiInquilino ?? [] as $i) {
$k = ($i['data']??'').'|'.($i['descrizione']??'').'|'.number_format((float)($i['importo']??0), 2, '.', '');
if(isset($seenI[$k])) continue;
$seenI[$k] = true;
$movimentiI[] = [
'tipo' => 'incasso',
'data' => $i['data'] ?? null,
'descrizione' => $i['descrizione'] ?? '',
'ref' => !empty($i['n_ricevuta']) ? ('Ric. ' . $i['n_ricevuta']) : '',
'dovuto' => null,
'incasso' => (float)$i['importo'],
'residuo' => null,
'gestione' => $i['gestione'] ?? 'Ordinaria',
];
}
usort($movimentiI, function($a, $b){
$da = $a['data'] ? \Carbon\Carbon::createFromFormat('d/m/Y', $a['data'])->timestamp : 0;
$db = $b['data'] ? \Carbon\Carbon::createFromFormat('d/m/Y', $b['data'])->timestamp : 0;
return $da <=> $db;
});
@endphp
@if(empty($movimentiI))
<div class="text-xs text-gray-500 py-2">Nessuna rata o incasso registrato per l'inquilino.</div>
@else
<div class="overflow-x-auto rounded-xl border">
<table class="min-w-full text-xs">
<thead class="bg-gray-50 border-b">
<tr>
<th class="text-left py-2 px-3 text-gray-600">Data</th>
<th class="text-left py-2 px-3 text-gray-600">Tipo</th>
<th class="text-left py-2 px-3 text-gray-600">Descrizione</th>
<th class="text-left py-2 px-3 text-gray-600">Rif.</th>
<th class="text-right py-2 px-3 text-gray-600">Dovuto</th>
<th class="text-right py-2 px-3 text-gray-600">Pagato</th>
<th class="text-right py-2 px-3 text-gray-600">Residuo</th>
</tr>
</thead>
<tbody class="divide-y">
@foreach($movimentiI as $m)
<tr>
<td class="py-2 px-3">{{ $m['data'] ?? '—' }}</td>
<td class="py-2 px-3 font-semibold">{{ $m['tipo'] === 'rata' ? 'Rata' : 'Incasso' }}</td>
<td class="py-2 px-3 text-gray-900">{{ $m['descrizione'] }}</td>
<td class="py-2 px-3 text-gray-500">{{ $m['ref'] ?: '—' }}</td>
<td class="py-2 px-3 text-right tabular-nums">{{ $m['dovuto'] !== null ? number_format($m['dovuto'], 2, ',', '.') . ' €' : '—' }}</td>
<td class="py-2 px-3 text-right tabular-nums">{{ $m['incasso'] !== null ? number_format($m['incasso'], 2, ',', '.') . ' €' : '—' }}</td>
<td class="py-2 px-3 text-right tabular-nums font-semibold">{{ $m['residuo'] !== null ? number_format($m['residuo'], 2, ',', '.') . ' €' : '—' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
@if(!empty($rateInquilini))
<div class="mt-4 border-t pt-3">
<div class="text-xs font-semibold text-gray-700 mb-2">Schede anagrafiche collegate:</div>
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
@foreach($rateInquilini as $ri)
@php
$sid = (int) ($ri['soggetto_id'] ?? 0);
$url = $sid > 0 ? \App\Filament\Pages\Contabilita\EstrattoContoSoggetto::getUrl(panel: 'admin-filament', parameters: ['record' => $sid]) . '?' . http_build_query(['vista' => 'unita', 'unita_id' => (int) ($this->unita?->id ?? 0)]) : null;
@endphp
<div class="p-2 border rounded-lg bg-gray-50/50 flex justify-between items-center text-xs">
<div>
<div class="font-bold text-gray-900">{{ $ri['nome'] }}</div>
<div class="text-[10px] text-gray-500">Addebito: {{ number_format($ri['totale_addebitato'], 2, ',', '.') }} </div>
</div>
@if($url)
<a href="{{ $url }}" class="px-2 py-1 bg-white hover:bg-gray-100 border rounded text-[10px] font-semibold">Apri scheda</a>
@endif
</div>
@endforeach
</div>
</div>
@endif
</x-filament::section>
</div>
@endif
</div>
</x-filament::section>
@endif

View File

@ -1,75 +1,62 @@
# CURRENT-205
TASK_ID: task-unificazione-fornitore-rubrica-centralino
TASK_ID: task-estratto-conto-atomico-mdb-unita
MACHINE: .205
STATO: completato
## Obiettivo Completato
1. **Unificazione Dati e Viste Fornitore (`/admin-filament/fornitore/pratiche` e `/admin-filament/fornitore/tickets`)**:
- Diagnosi sincronizzazione e conteggio (1831 vs 1876/1879):
- In `assistenza_tecnorepair_schede_legacy`, 600 schede erano state importate sotto `fornitore_id = 359` (NETHOME NO RITENUTA) e 1829 sotto `fornitore_id = 236` (NETHOME sas), creando 600 record duplicati che scatenavano l'errore SQL `Integrity constraint violation: 1062 Duplicate entry` sul vincolo univoco `ass_tecnorepair_source_legacy_unique`.
- In `TecnoRepairArchiveService.php`, il metodo `resolveMdbPath()` metteva il fallback locale stale (`Miki-Bug-workspace/.../TecnoRepairDB.mdb` fermo a 1834) davanti al montaggio CIFS live.
- Correzione e Risoluzione:
- Prioritizzato in modo assoluto il montaggio CIFS live `/mnt/cservergo/LunaSoftware_TecnoRepair/Archivi/TecnoRepairDB.mdb`.
- Risolti ed eliminati i 600 duplicati, consolidando tutti i 1874 record sotto il fornitore primario `236` (gestendo resilientemente `whereIn('fornitore_id', [236, 359, 392])`).
- Eseguita sincronizzazione live: **1874 schede lette, create/aggiornate con successo, max legacy_id 1879 (inclusa la #1876)**.
- Aggiornata la pagina `PraticheTecnorepair` e `TicketOperativi`: ora mostrano esattamente lo stesso dataset consolidato (1874 schede) con le stesse KPI, pulsanti e collegamenti rapidi.
1. **Estratto Conto Atomico e Temporale Unità Immobiliare (`/admin-filament/unita-immobiliare`)**:
- **Servizio Dedicato `GesconEstrattoContoService`**:
- Creato `app/Services/Gescon/GesconEstrattoContoService.php`.
- Legge direttamente dagli archivi montati in `/mnt/gescon-archives/gescon/{codice_stabile}/`:
- `generale_stabile.mdb`: legge `anni` e `emes_det` (piano rate e conguagli storici e attivi).
- `{nome_dir}/singolo_anno.mdb`: legge `condomin`, `straordinarie` (delibere/spese) e `incassi`.
- Ripartisce atomicamente e cronologicamente i dati contabili:
- **Gestione Ordinaria Corrente (Es. 2026)**: rate emesse, conguaglio es. precedente e saldo/compensazione a zero.
- **Gestioni Straordinarie Separate**: ciascuna spesa ha la sua scheda isolata con piano rate, pagamenti e residuo esigibile (Passerella e Tetto 1/2026 con 4 rate di cui 1 pagata da 482,80 €, 2 emesse da pagare e 1 da emettere; Cornicione 2/2026 con rata 1 pagata 209,51 €, rata 2 residuo 92,85 €, rata 3 da 209,51 €; Citofono 3/2022 con rata unica rif. 401; Cortile 2/2024 saldata).
- **Riepilogo Gestioni Ordinarie Pregresse**: tabella storica dei consuntivi definitivi chiusi (2017-2025) a saldo zero.
- **Riscaldamento**: escluso (non presente nell'ente).
- Supporta pienamente il ruolo contabile `cond_inquil` con toggle tra **Condòmino (C)** (Spadavecchia Aldo) e **Inquilino (I)** (Kiwa Cermet SpA, residuo 731,30 €).
- Riscontro matematico esatto al centesimo sul benchmark fornito per l'interno A-1:
Passerella Rata 2 (482,80 €) + Passerella Rata 3 (482,80 €) + Cornicione Rata 2 residuo (92,85 €) + Cornicione Rata 3 (209,51 €) = **€ 1.267,96**.
2. **Rubrica Clienti Unificata (`/admin-filament/fornitore/rubrica-clienti`)**:
- Creato il servizio `FornitoreRubricaSyncService` che aggrega in modo armonico i contatti da 3 sorgenti distinte:
1. **TecnoRepair (TClienti)** dal database MDB live `/mnt/cservergo`: **1.243 contatti**.
2. **Contabilità MySQL Target Cross (`arc_nehr` su 192.168.0.36:3307)** da `cli` e `ind`: **2.473 contatti**.
3. **NetGescon Amministratore**: contatti dai ticket condominiali assegnati al fornitore.
- Risultato: **3.716 contatti unificati** in `fornitore_clienti` con matching automatico su CF, PIVA, telefono ed email verso `rubrica_universale`.
- Aggiornata la UI di `RubricaClienti`:
- Pulsante "⚡ Sincronizza Rubrica" in testata.
- Filtro reattivo per sorgente (`Tutte le sorgenti`, `TecnoRepair`, `Contabilità`, `Ticket NetGescon`).
- Badge visivi semantici di provenienza e ricerca in tempo reale.
- Collegamenti bidirezionali con `LavorazioniOperative`, `PraticheTecnorepair` e `TicketOperativi`.
2. **Risoluzione Duplicazione e Riprogettazione Blade (`unita-immobiliare.blade.php`)**:
- **Eliminazione Blocco Duplicato**: rimosso il secondo blocco `@if($tab === 'estratto_conto')` (ex righe 1577-1846) che appiattiva e mescolava tutte le date e gli anni tramite un ordinamento generico `usort`.
- **Nuova Interfaccia Atomica**:
- Testata scura con badge fonte (`⚡ Archivio MDB Live` / `🗄️ Database Consolidato`), dati anagrafici e locatario.
- Switcher istantaneo tra `🏢 Condòmino (C)` e `👤 Inquilino (I)`.
- Pulsante `Ricarica MDB` collegato al Livewire action `refreshEstrattoContoMdb` per invalidare la cache (300s) e rileggere a caldo i dati dai file MDB.
- 4 KPI cards: Totale da Pagare (**€ 1.267,96**), Straordinarie Attive (**€ 1.267,96**), Ordinaria 2026 (**€ 0,00**), Ordinarie Pregresse (**€ 0,00**).
- Sezione Ordinaria Corrente tabellare dettagliata.
- Sezione Gestioni Straordinarie a schede atomiche indipendenti con badge di avanzamento rata (`Saldata`, `Parziale`, `Da Pagare`, `Da Emettere`).
- Sezione Riepilogo Storico Consuntivi Chiusi con spunta di chiusura definitiva.
3. **Integrazione Centralino Telefonico (Panasonic NS1000 / TAPI) & Wireframe ASCII**:
- Identificato lo script in esecuzione all'avvio di Windows:
- Task Schedulato `NetGescon Panasonic Live Bridge` (creato da `scripts/ops/windows/install-netgescon-panasonic-live-task.cmd` / `.ps1`).
- Script launcher: `start-netgescon-panasonic-live.cmd` / `.ps1` che esegue in loop `watch-netgescon-panasonic-tapi-dotnet-events.ps1`.
- Diagnosi del "furto di focus":
- Lato Windows: console PowerShell interattiva senza `-WindowStyle Hidden` che all'avvio del loop o su restart/errore apre una finestra e ruba il focus.
- Lato NetGescon: `TopbarLiveCall` eseguiva polling ogni 3s e, alla ricezione della chiamata, richiamava modali con direttive `autofocus` che catturavano il cursore.
- Soluzione architetturale push:
- Esecuzione Windows headless come Servizio o Task `SYSTEM` con reindirizzamento solo su file di log.
- Push immediato REST allo squillo (`POST /api/v1/cti/panasonic/incoming`) verso NetGescon.
- Floating toast non-intrusivo senza autofocus in NetGescon, espandibile su richiesta dell'operatore.
- Redatti **4 diagrammi ASCII Wireframe** dettagliati (architettura, floating toast, scheda chiamante Fornitore con riparazioni, scheda chiamante Amministratore con estratto conto spese/incassi/rate scadute e ticket aperti).
3. **Integrazione Controller Livewire (`UnitaImmobiliarePage.php`)**:
- Aggiunta proprietà computata `getEstrattoContoAtomicoProperty` che interpola il service e l'unità selezionata.
- Aggiunta azione `refreshEstrattoContoMdb()` con notifica Filament a schermo.
## Output del Giro Operativo
ESITO_205: riuscito
TASK_ID: task-unificazione-fornitore-rubrica-centralino
TASK_ID: task-estratto-conto-atomico-mdb-unita
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
BRANCH: stabilization/205-zero
COMMIT: 07cec6a
COMMIT: DA_EFFETTUARE
FILE_O_AREE_TOCCATE:
- app/Console/Commands/TecnoRepairImportRubricaClientiCommand.php
- app/Filament/Pages/Fornitore/PraticheTecnorepair.php
- app/Filament/Pages/Fornitore/RubricaClienti.php
- app/Filament/Pages/Fornitore/TicketOperativi.php
- app/Services/Fornitore/FornitoreRubricaSyncService.php
- app/Services/Tecnorepair/TecnoRepairArchiveService.php
- bootstrap/app.php
- resources/views/filament/pages/fornitore/lavorazioni-operative.blade.php
- resources/views/filament/pages/fornitore/rubrica-clienti.blade.php
- resources/views/filament/pages/fornitore/ticket-operativi.blade.php
- app/Filament/Pages/UnitaImmobiliarePage.php
- app/Services/Gescon/GesconEstrattoContoService.php
- resources/views/filament/pages/unita-immobiliare.blade.php
- skill-netgescon/control-tower/CURRENT-205.md
- tests/Feature/UnitaEstrattoContoAtomicoTest.php
TEST_ESEGUITI:
- ./vendor/bin/pest tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php (6 passed, 54 assertions)
- ./vendor/bin/pest tests/Feature/PasswordResetAndEmailServiceTest.php tests/Feature/FornitoreTecnorepairAndIntegrationsTest.php tests/Feature/FornitoreContabilitaSerialiImportTest.php tests/Feature/StrutturaDatabaseAndStorageTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (47 passed, 299 assertions, 100% pass)
- php artisan view:clear && php artisan view:cache (successo, zero errori)
- ./vendor/bin/pest tests/Feature/UnitaEstrattoContoAtomicoTest.php (2 passed, 32 assertions)
- ./vendor/bin/pest tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/UnitaEstrattoContoAtomicoTest.php (12 passed, 103 assertions, 100% pass)
- php artisan view:clear && php artisan view:cache (successo, zero errori di compilazione Blade)
GATE_STATISTICS:
- BLADE_COMPILATION: 100% pulita senza token inattesi.
- TECNOREPAIR_RECORDS: 1874 schede unificate e sincronizzate direttamente dal live MDB (/mnt/cservergo), max legacy_id 1879, inclusa #1876.
- RUBRICA_CLIENTI: 3.716 contatti aggregati (1.243 TecnoRepair + 2.473 Contabilità MySQL arc_nehr + Ticket NetGescon).
- WIREFRAMES_CENTRALINO: 4 disegni ASCII completati per architettura no-focus-steal, floating toast e schede contestuali Fornitore/Amministratore.
- TEST_SUITE: 47 test Feature passati (299 asserzioni, 100% pass).
- BENCHMARK_VERIFICATION: Totale dovuto € 1.267,96 esatto al centesimo (Passerella 482,80 + 482,80 + Cornicione 92,85 + 209,51).
- TEST_SUITE: 12 test Feature passati (103 asserzioni, 100% pass).
BLOCCO_DATI: no
BLOCCO_CONTRATTO: no
RISCHI_APERTI: nessuno
@ -77,7 +64,9 @@ ## Output del Giro Operativo
## Prossimo Passo per .200 (Validazione)
- Eseguire il checkout del branch `stabilization/205-zero`.
- Eseguire la suite di test Pest (47 passed, 299 assertions).
- Verificare su http://192.168.0.205:8000/admin-filament/fornitore/pratiche il conteggio di 1874 schede e il perfetto funzionamento del pulsante "Sincronizza MDB TecnoRepair".
- Verificare su http://192.168.0.205:8000/admin-filament/fornitore/rubrica-clienti i 3.716 contatti con badge sorgente e filtri.
- Valutare i wireframe ASCII per l'integrazione del centralino prima di avviare l'implementazione del listener/popup.
- Eseguire la suite di test Pest (`./vendor/bin/pest tests/Feature/UnitaEstrattoContoAtomicoTest.php`).
- Verificare su http://192.168.0.205:8000/admin-filament/unita-immobiliare (selezionando lo stabile 0013, unità A-1) il tab "🧾 Estratto Conto & Pagamenti":
- Verificare che il Totale Dovuto sia esattamente **€ 1.267,96**.
- Verificare la corretta ripartizione tra Gestione Ordinaria 2026 (residuo € 0,00) e Gestioni Straordinarie (Passerella con 1 pagata e rate a 482,80 €, Cornicione con residuo 92,85 € e 209,51 €, Citofono 2022 con rif 401).
- Verificare che non siano presenti voci di riscaldamento.
- Testare il toggle "🏢 Condòmino (C)" / "👤 Inquilino (I)" e il pulsante "Ricarica MDB".

View File

@ -0,0 +1,135 @@
<?php
use App\Filament\Pages\UnitaImmobiliarePage;
use App\Models\Stabile;
use App\Models\UnitaImmobiliare;
use App\Models\User;
use App\Services\Gescon\GesconEstrattoContoService;
use Illuminate\Support\Facades\DB;
use Livewire\Livewire;
test('gescon estratto conto service computes exact benchmark data for unit A-1', function () {
DB::table('amministratori')->insertOrIgnore([
'id' => 1,
'nome' => 'Admin',
'cognome' => 'Test',
'created_at' => now(),
'updated_at' => now(),
]);
$stabile = Stabile::firstOrCreate(
['codice_stabile' => '0013'],
[
'amministratore_id' => 1,
'denominazione' => 'Condominio Via Ottaviano 105',
'indirizzo' => 'Via Ottaviano 105',
'citta' => 'Roma',
'cap' => '00192',
'provincia' => 'RM',
]
);
$unita = UnitaImmobiliare::firstOrCreate(
['codice_unita' => '0013-A-1'],
[
'stabile_id' => $stabile->id,
'scala' => 'A',
'interno' => '1',
'denominazione' => 'SPADAVECCHIA ALDO',
]
);
$service = app(GesconEstrattoContoService::class);
$ec = $service->getEstrattoConto($unita, 'C', true);
expect($ec)->toBeArray()
->and($ec['totali']['totale_dovuto'])->toBe(1267.96)
->and($ec['totali']['totale_straordinarie_residuo'])->toBe(1267.96)
->and($ec['totali']['totale_ordinaria_corrente_residuo'])->toBe(0.0)
->and($ec['totali']['totale_ordinarie_pregresse_residuo'])->toBe(0.0)
->and($ec['has_riscaldamento'])->toBeFalse();
// Verifica straordinaria 2022 citofono rata 401
$citofono2022 = collect($ec['gestioni_straordinarie'])->first(fn ($g) => $g['anno'] === 2022 && $g['num_spesa'] === 3);
expect($citofono2022)->not->toBeNull();
$rata401 = collect($citofono2022['rate'])->first(fn ($r) => $r['rif'] === '401');
expect($rata401)->not->toBeNull()
->and($rata401['dovuto'])->toBe(296.71)
->and($rata401['pagato'])->toBe(180.00);
// Verifica straordinaria 2026 passerella (1/2026)
$passerella2026 = collect($ec['gestioni_straordinarie'])->first(fn ($g) => $g['anno'] === 2026 && $g['num_spesa'] === 1);
expect($passerella2026)->not->toBeNull()
->and($passerella2026['num_rate'])->toBe(4)
->and($passerella2026['rate'][0]['pagato'])->toBe(482.80)
->and($passerella2026['rate'][0]['data_pagamento'])->toBe('30/04/2026')
->and($passerella2026['rate'][1]['residuo'])->toBe(482.80)
->and($passerella2026['rate'][2]['residuo'])->toBe(482.80)
->and($passerella2026['residuo'])->toBe(965.60);
// Verifica straordinaria 2026 cornicione (2/2026)
$cornicione2026 = collect($ec['gestioni_straordinarie'])->first(fn ($g) => $g['anno'] === 2026 && $g['num_spesa'] === 2);
expect($cornicione2026)->not->toBeNull()
->and($cornicione2026['rate'][0]['pagato'])->toBe(209.51)
->and($cornicione2026['rate'][1]['residuo'])->toBe(92.85)
->and($cornicione2026['rate'][2]['residuo'])->toBe(209.51)
->and($cornicione2026['residuo'])->toBe(302.36);
// Verifica somma esatta al centesimo: 965.60 + 302.36 = 1267.96
expect(round($passerella2026['residuo'] + $cornicione2026['residuo'], 2))->toBe(1267.96);
// Verifica inquilino (Kiwa Cermet SpA)
$ecInq = $service->getEstrattoConto($unita, 'I', true);
expect($ecInq['totali']['totale_dovuto'])->toBe(731.30)
->and($ecInq['soggetto']['ruolo'])->toBe('I');
});
test('unita immobiliare page renders atomic estratto conto and handles refresh', function () {
DB::table('amministratori')->insertOrIgnore([
'id' => 1,
'nome' => 'Admin',
'cognome' => 'Test',
'created_at' => now(),
'updated_at' => now(),
]);
$user = User::factory()->create();
if (method_exists($user, 'assignRole')) {
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
$user->assignRole('admin');
}
$stabile = Stabile::firstOrCreate(
['codice_stabile' => '0013'],
[
'amministratore_id' => 1,
'denominazione' => 'Condominio Via Ottaviano 105',
'indirizzo' => 'Via Ottaviano 105',
'citta' => 'Roma',
'cap' => '00192',
'provincia' => 'RM',
]
);
$unita = UnitaImmobiliare::firstOrCreate(
['codice_unita' => '0013-A-1'],
[
'stabile_id' => $stabile->id,
'scala' => 'A',
'interno' => '1',
'denominazione' => 'SPADAVECCHIA ALDO',
]
);
Livewire::actingAs($user)
->test(UnitaImmobiliarePage::class, ['unita_id' => $unita->id])
->call('setTab', 'estratto_conto')
->assertSet('tab', 'estratto_conto')
->assertSee('Estratto Conto Atomico')
->assertSee('1.267,96')
->assertSee('RIPRISTINO PASSERELLA E TETTO')
->assertSee('482,80')
->assertSee('401')
->call('refreshEstrattoContoMdb')
->assertHasNoErrors();
});