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:
parent
a79598e66a
commit
f4d2050a92
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' => [],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user