From e9cf216fa406b5336bb0a2e0d73877c8321b4914 Mon Sep 17 00:00:00 2001 From: michele Date: Tue, 21 Apr 2026 17:57:51 +0000 Subject: [PATCH] Refine water services and bank movement views --- CHANGELOG.md | 1 + VERSION | 2 +- .../Pages/Affitti/GestioneAffitti.php | 69 +++++++- .../Condomini/ServiziStabileArchivio.php | 26 +++ .../Contabilita/CasseBancheMovimenti.php | 161 +----------------- app/Models/AffittoImmobile.php | 2 + .../movimento-banca-dettaglio.blade.php | 31 +++- .../pages/gescon/fornitore-scheda.blade.php | 78 +++++---- 8 files changed, 179 insertions(+), 191 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d2d7ce..8a3c795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ # Changelog ## [Unreleased] +- Surfaced ACQUA invoices in the servizi-utenze area even when the supplier is only flagged via FE water features, compacted the supplier scheda actions into a single toolbar without page-level horizontal overflow, standardized the bank movements grid to the requested visible columns with a richer raw-data modal, and made affitti canoni start from the effective management takeover date. - Reworked the banking operations hub so the topbar stays full-width, the sidebar stays fixed below it, the movements tab remains focused on imported/manual bank movements, and archive or gestione structure moved into a dedicated tab. The same slice also added first-pass QIF cleanup metadata for CBILL, SIA, commissions and provider hints, plus stronger bank-to-supplier matching and ACQ assignment for water invoices extracted from FE/PDF data. - Fixed document archive visibility queries to work safely even when single-user ACL columns are not yet present in older databases, and added the missing `documenti.allowed_user_ids` schema field for per-document access control. - Restored a cleaner two-column layout in Post-it Gestione boards and hid visual noise from calls that terminate on internal response group `601` while keeping the underlying records in the system. diff --git a/VERSION b/VERSION index 100435b..ee94dd8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.2 +0.8.3 diff --git a/app/Filament/Pages/Affitti/GestioneAffitti.php b/app/Filament/Pages/Affitti/GestioneAffitti.php index ce69960..c09c926 100644 --- a/app/Filament/Pages/Affitti/GestioneAffitti.php +++ b/app/Filament/Pages/Affitti/GestioneAffitti.php @@ -621,6 +621,7 @@ private function loadAffitti(): void 'nome_inquilino' => $a->nome_inquilino, 'proprietario_nome' => $a->proprietario_nome, 'inizio_contratto' => $a->inizio_contratto, + 'presa_in_carico_operativa_dal' => $a->presa_in_carico_operativa_dal, 'prossima_registrazione' => $a->prossima_registrazione, 'tipo_riga' => $a->tipo_riga, 'stabile_id' => $a->stabile_id, @@ -657,12 +658,44 @@ private function buildAffittoWarnings(AffittoImmobile $affitto): array } } + $managementStart = $this->resolveCanoniManagementStart($affitto); + $scadutiNonPagati = AffittoCanoneDovuto::query() ->where('affitto_id', $affitto->id) + ->when($managementStart, function ($query) use ($managementStart) { + $query->where(function ($inner) use ($managementStart) { + $inner->whereDate('data_scadenza', '>=', $managementStart->toDateString()) + ->orWhere(function ($fallback) use ($managementStart) { + $fallback->whereNull('data_scadenza') + ->where(function ($monthQuery) use ($managementStart) { + $monthQuery->where('anno', '>', (int) $managementStart->year) + ->orWhere(function ($sameYear) use ($managementStart) { + $sameYear->where('anno', (int) $managementStart->year) + ->where('mese', '>=', (int) $managementStart->month); + }); + }); + }); + }); + }) ->whereDate('data_scadenza', '<', $today->toDateString()) ->sum('totale') - AffittoCanonePagato::query() ->where('affitto_id', $affitto->id) + ->when($managementStart, function ($query) use ($managementStart) { + $query->where(function ($inner) use ($managementStart) { + $inner->whereDate('data_pagamento', '>=', $managementStart->toDateString()) + ->orWhere(function ($fallback) use ($managementStart) { + $fallback->whereNull('data_pagamento') + ->where(function ($monthQuery) use ($managementStart) { + $monthQuery->where('anno', '>', (int) $managementStart->year) + ->orWhere(function ($sameYear) use ($managementStart) { + $sameYear->where('anno', (int) $managementStart->year) + ->where('mese', '>=', (int) $managementStart->month); + }); + }); + }); + }); + }) ->whereDate('data_pagamento', '<=', $today->toDateString()) ->sum('totale'); @@ -786,6 +819,7 @@ private function loadSelected(): void 'numero_registrazione_ade' => null, 'data_registrazione_ade' => null, 'inizio_contratto' => null, + 'presa_in_carico_operativa_dal' => null, 'ultimo_rinnovo' => null, 'prossima_scadenza' => null, 'prossima_registrazione' => null, @@ -852,8 +886,9 @@ private function loadSelected(): void $this->ensureRicevute($record, $dovuti); $pagati = AffittoCanonePagato::where('affitto_id', $record->id)->orderByDesc('anno')->orderByDesc('mese')->get(); - if (! empty($record->inizio_contratto)) { - $start = Carbon::parse($record->inizio_contratto)->startOfMonth(); + $managementStart = $this->resolveCanoniManagementStart($record); + if ($managementStart) { + $start = $managementStart->copy()->startOfMonth(); $dovuti = $dovuti->filter(function (AffittoCanoneDovuto $row) use ($start) { $month = Carbon::create((int) $row->anno, (int) $row->mese, 1); return $month->gte($start); @@ -1219,11 +1254,12 @@ private function emitCanoniAutomaticiForRecord(AffittoImmobile $record): int return 0; } - if (empty($record->inizio_contratto)) { + $managementStart = $this->resolveCanoniManagementStart($record); + if (! $managementStart) { return 0; } - $start = Carbon::parse($record->inizio_contratto)->startOfMonth(); + $start = $managementStart->copy()->startOfMonth(); $todayMonth = Carbon::today()->startOfMonth(); $end = $todayMonth; @@ -1406,6 +1442,31 @@ private function emitCanoniAutomaticiForRecord(AffittoImmobile $record): int return $created; } + private function resolveCanoniManagementStart(AffittoImmobile $record): ?Carbon + { + $dates = []; + + if (! empty($record->inizio_contratto)) { + $dates[] = Carbon::parse($record->inizio_contratto)->startOfMonth(); + } + + if (! empty($record->presa_in_carico_operativa_dal)) { + $dates[] = Carbon::parse($record->presa_in_carico_operativa_dal)->startOfMonth(); + } + + if ($record->stabile && $record->stabile->latestAmministratoreTransfer?->created_at) { + $dates[] = $record->stabile->latestAmministratoreTransfer->created_at->copy()->startOfMonth(); + } + + if ($dates === []) { + return null; + } + + usort($dates, fn(Carbon $left, Carbon $right): int => $left->getTimestamp() <=> $right->getTimestamp()); + + return $dates[count($dates) - 1]; + } + private function resolveIstatQuotaFromClause(AffittoContrattoClausola $clausola): float { $content = trim((string) ($clausola->titolo ?? '') . ' ' . (string) ($clausola->testo ?? '')); diff --git a/app/Filament/Pages/Condomini/ServiziStabileArchivio.php b/app/Filament/Pages/Condomini/ServiziStabileArchivio.php index 437aa9f..4040830 100644 --- a/app/Filament/Pages/Condomini/ServiziStabileArchivio.php +++ b/app/Filament/Pages/Condomini/ServiziStabileArchivio.php @@ -1429,6 +1429,32 @@ public function getAcquaFatturePerGestioneProperty(): array ->values() ->all(); + if (Schema::hasTable('fornitori') && Schema::hasColumn('fornitori', 'fe_features')) { + $waterEnabledSupplierIds = Fornitore::query() + ->where(function (Builder $query) use ($stabileId): Builder { + return $query->whereExists(function ($subQuery) use ($stabileId) { + $subQuery->selectRaw('1') + ->from('fatture_elettroniche as fe') + ->whereColumn('fe.fornitore_id', 'fornitori.id') + ->where('fe.stabile_id', $stabileId); + })->orWhereExists(function ($subQuery) use ($stabileId) { + $subQuery->selectRaw('1') + ->from('contabilita_fatture_fornitori as cff') + ->whereColumn('cff.fornitore_id', 'fornitori.id') + ->where('cff.stabile_id', $stabileId); + }); + }) + ->whereRaw("JSON_EXTRACT(fe_features, '$.acqua.enabled') = true") + ->pluck('id') + ->map(fn($v) => (int) $v) + ->filter(fn($v) => $v > 0) + ->unique() + ->values() + ->all(); + + $fornitoreIds = array_values(array_unique(array_merge($fornitoreIds, $waterEnabledSupplierIds))); + } + if ($fornitoreIds !== []) { $contabili = DB::table('contabilita_fatture_fornitori as f') ->leftJoin('gestioni_contabili as g', 'g.id', '=', 'f.gestione_id') diff --git a/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php b/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php index fcaaed7..a3013fb 100644 --- a/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php +++ b/app/Filament/Pages/Contabilita/CasseBancheMovimenti.php @@ -1305,149 +1305,6 @@ public function table(Table $table): Table }), ]) ->columns([ - TextColumn::make('codice_descrizione') - ->label('Codice') - ->state(function (MovimentoBanca $record): string { - $descr = (string) ($record->descrizione ?? ''); - $raw = strtoupper($descr); - if (str_contains($raw, 'ACCREDITO_BEU_CON_CONTABILE')) { - return 'ACCBEUCONCON'; - } - if (str_contains($raw, 'COMBONMYB')) { - return 'COMBONMYB'; - } - $raw = preg_replace('/[^A-Z0-9]+/', ' ', $raw) ?? $raw; - $parts = array_filter(explode(' ', $raw), fn($v) => $v !== ''); - if (! empty($parts)) { - $code = ''; - foreach ($parts as $p) { - $chunk = substr($p, 0, 3); - $code .= $chunk; - if (strlen($code) >= 12) { - break; - } - } - $code = substr($code, 0, 12); - if ($code !== '') { - return $code; - } - } - $raw = preg_replace('/[^A-Z0-9]+/', '_', $raw) ?? $raw; - $raw = trim($raw, '_'); - $raw = preg_replace('/_+/', '_', $raw) ?? $raw; - return mb_substr($raw, 0, 32); - }) - ->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('disp') - ->label('Disp') - ->state(function (MovimentoBanca $record): string { - $match = is_array($record->match_data ?? null) ? $record->match_data : []; - $disp = $match['cod_disp'] ?? null; - return is_string($disp) ? trim($disp) : ''; - }) - ->toggleable(), - TextColumn::make('cash') - ->label('Cash') - ->state(function (MovimentoBanca $record): string { - $match = is_array($record->match_data ?? null) ? $record->match_data : []; - $cash = $match['cash'] ?? null; - return is_string($cash) ? trim($cash) : ''; - }) - ->toggleable(), - TextColumn::make('mittente') - ->label('Mittente') - ->state(function (MovimentoBanca $record): string { - $match = is_array($record->match_data ?? null) ? $record->match_data : []; - $mitt = $match['mittente'] ?? null; - return is_string($mitt) ? trim($mitt) : ''; - }) - ->wrap() - ->toggleable(), - BadgeColumn::make('commissioni') - ->label('Comm.') - ->getStateUsing(function (MovimentoBanca $record): string { - $raw = strtoupper((string) ($record->descrizione ?? '')); - return str_contains($raw, 'COMBONMYB') ? 'si' : 'no'; - }) - ->colors([ - 'success' => 'si', - 'gray' => 'no', - ]) - ->formatStateUsing(fn(string $state): string => $state === 'si' ? 'SI' : '—') - ->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('disp') - ->label('Disp') - ->state(function (MovimentoBanca $record): string { - $match = is_array($record->match_data ?? null) ? $record->match_data : []; - $disp = $match['cod_disp'] ?? null; - return is_string($disp) ? trim($disp) : ''; - }) - ->toggleable(), - BadgeColumn::make('da_confermare') - ->label('Conferma') - ->getStateUsing(fn(MovimentoBanca $record): string => ! empty($record->da_confermare) ? 'da_confermare' : 'ok') - ->colors([ - 'warning' => 'da_confermare', - 'success' => 'ok', - ]) - ->formatStateUsing(fn(string $state): string => $state === 'da_confermare' ? 'DA CONF.' : 'OK') - ->toggleable(isToggledHiddenByDefault: true), - - BadgeColumn::make('stato_quadratura') - ->label('Stato') - ->getStateUsing(fn(MovimentoBanca $record): string => $this->resolveStatoQuadratura($record)) - ->colors([ - 'gray' => 'non_collegato', - 'warning' => 'collegato_non_quadrato', - 'success' => 'quadrato', - ]) - ->formatStateUsing(function (string $state): string { - return match ($state) { - 'quadrato' => 'OK', - 'collegato_non_quadrato' => 'ATT', - default => 'NO', - }; - }) - ->sortable(false), - - TextColumn::make('voci_collegate') - ->label('Voce spesa') - ->state(function (MovimentoBanca $record): string { - if (! $record->registrazione_id || ! Schema::hasColumn('contabilita_movimenti', 'voce_spesa_id')) { - return '—'; - } - - static $cache = []; - $regId = (int) $record->registrazione_id; - if (array_key_exists($regId, $cache)) { - return $cache[$regId]; - } - - $rows = DB::table('contabilita_movimenti as m') - ->leftJoin('voci_spesa as v', 'v.id', '=', 'm.voce_spesa_id') - ->where('m.registrazione_id', $regId) - ->whereNotNull('m.voce_spesa_id') - ->select('v.codice', 'v.descrizione') - ->distinct() - ->limit(5) - ->get(); - - if ($rows->isEmpty()) { - return $cache[$regId] = '—'; - } - - $labels = []; - foreach ($rows as $row) { - $code = trim((string) ($row->codice ?? '')); - $desc = trim((string) ($row->descrizione ?? '')); - $labels[] = $code !== '' ? ($code . ($desc !== '' ? ' · ' . $desc : '')) : ($desc !== '' ? $desc : 'Voce'); - } - - return $cache[$regId] = implode(' | ', array_values(array_unique($labels))); - }) - ->wrap() - ->toggleable(), - TextColumn::make('data') ->label('Data') ->date('d/m/Y') @@ -1455,8 +1312,10 @@ public function table(Table $table): Table TextColumn::make('valuta') ->label('Valuta') - ->date('d/m/Y') - ->toggleable(isToggledHiddenByDefault: true), + ->date('d/m/Y'), + + TextColumn::make('causale') + ->label('Caus'), TextColumn::make('descrizione') ->label('Descrizione') @@ -1466,7 +1325,8 @@ public function table(Table $table): Table TextColumn::make('gestioneContabile.denominazione') ->label('Gestione') - ->toggleable(isToggledHiddenByDefault: true), + ->toggleable(isToggledHiddenByDefault: true) + ->toggledHiddenByDefault(), TextColumn::make('entrate') ->label('Entrate') @@ -1492,17 +1352,14 @@ public function table(Table $table): Table ->toggleable(isToggledHiddenByDefault: true) ->formatStateUsing(fn($state) => '€ ' . number_format((float) $state, 2, ',', '.')), - TextColumn::make('causale') - ->label('Caus.') - ->toggleable(), - TextColumn::make('iban') ->label('IBAN') - ->toggleable(isToggledHiddenByDefault: true), + ->toggleable(isToggledHiddenByDefault: true) + ->toggledHiddenByDefault(), ]) ->actions([ Action::make('dettaglio_movimento') - ->label('Dettaglio') + ->label('Apri dati') ->icon('heroicon-o-eye') ->modalWidth('7xl') ->action(function (MovimentoBanca $record): void { diff --git a/app/Models/AffittoImmobile.php b/app/Models/AffittoImmobile.php index 5fe7b46..5eda845 100644 --- a/app/Models/AffittoImmobile.php +++ b/app/Models/AffittoImmobile.php @@ -41,6 +41,7 @@ class AffittoImmobile extends Model 'numero_registrazione_ade', 'data_registrazione_ade', 'inizio_contratto', + 'presa_in_carico_operativa_dal', 'ultimo_rinnovo', 'prossima_scadenza', 'prossima_registrazione', @@ -77,6 +78,7 @@ class AffittoImmobile extends Model 'data_registrazione_ade' => 'date', 'obsoleto' => 'boolean', 'obsoleto_dal' => 'date', + 'presa_in_carico_operativa_dal' => 'date', 'giorno_emissione_canone' => 'integer', 'mese_adempimenti_annuali' => 'integer', 'giorno_adempimenti_annuali' => 'integer', diff --git a/resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php b/resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php index f00956a..7819deb 100644 --- a/resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php +++ b/resources/views/filament/pages/contabilita/partials/movimento-banca-dettaglio.blade.php @@ -23,10 +23,18 @@
Data
{{ optional($record->data)->format('d/m/Y') ?: '—' }}
+
+
Valuta
+
{{ optional($record->valuta)->format('d/m/Y') ?: '—' }}
+
Importo
€ {{ number_format((float) $record->importo, 2, ',', '.') }}
+
+
Causale
+
{{ $record->causale ?? '—' }}
+
Descrizione
{{ $record->descrizione ?? '—' }}
@@ -35,6 +43,20 @@
Descrizione estesa
{{ $record->descrizione_estesa_pulita ?? $record->descrizione_estesa ?? '—' }}
+
+
Saldo progressivo
+
+ @if(isset($record->saldo_progressivo) && is_numeric($record->saldo_progressivo)) + € {{ number_format((float) $record->saldo_progressivo, 2, ',', '.') }} + @else + — + @endif +
+
+
+
File sorgente
+
{{ $record->source_file ?? '—' }}
+
Disp
{{ is_array($record->match_data ?? null) ? ($record->match_data['cod_disp'] ?? '—') : '—' }}
@@ -47,6 +69,10 @@
Mittente
{{ is_array($record->match_data ?? null) ? ($record->match_data['mittente'] ?? '—') : '—' }}
+
+
Riga raw importata
+
{{ $record->raw_line ?? '—' }}
+
@php @@ -63,10 +89,5 @@ @endif -
- Conferma - Genera prima nota - Paga fornitore -
@endif diff --git a/resources/views/filament/pages/gescon/fornitore-scheda.blade.php b/resources/views/filament/pages/gescon/fornitore-scheda.blade.php index 9fce986..5d7be47 100644 --- a/resources/views/filament/pages/gescon/fornitore-scheda.blade.php +++ b/resources/views/filament/pages/gescon/fornitore-scheda.blade.php @@ -7,9 +7,21 @@ 'catalogo' => 'Catalogo e prodotti', 'contabilita' => 'Contabilita', ]; + $catalogoActions = [ + ['action' => 'nuovo_prodotto', 'label' => 'Nuovo prodotto', 'color' => 'gray'], + ['action' => 'estrai_prodotti_fe', 'label' => 'Estrai da FE', 'color' => 'primary'], + ['action' => 'importa_listino_csv', 'label' => 'Importa listino CSV', 'color' => 'success'], + ['action' => 'scarica_asset_catalogo', 'label' => 'Internalizza asset', 'color' => 'warning'], + ['action' => 'genera_referral_amazon', 'label' => 'Prepara link Amazon', 'color' => 'gray'], + ]; + $assistenzaActions = [ + ['action' => 'impostazioni_fe', 'label' => 'Impostazioni FE', 'color' => 'gray'], + ['action' => 'acqua_config', 'label' => 'Configura acqua', 'color' => 'gray'], + ['action' => 'acqua_scan', 'label' => 'Scansiona FE acqua', 'color' => 'primary'], + ]; @endphp -
+
@@ -41,6 +53,37 @@ class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium" Qui restano visibili modulo fornitore e catalogo operativo. Il magazzino e la gestione completa del prodotto possono essere spostati in menu dedicati mantenendo qui il quadro sintetico e i collegamenti rapidi.
+
+
+
+
Azioni catalogo
+
Azioni automazioni
+
Un solo punto per le azioni operative del catalogo, senza duplicarle in più riquadri.
+
FE e acqua raccolte in una toolbar compatta, così la colonna destra non spinge più il layout.
+
+
+ @foreach($catalogoActions as $action) + {{ $action['label'] }} + @endforeach + @foreach($assistenzaActions as $action) + {{ $action['label'] }} + @endforeach +
+
+
+
Questa tab concentra TecnoRepair, seriali e automazioni collegate al fornitore. I ticket operativi e il protocollo comunicazioni possono poi evolvere come viste dedicate, senza ricadere in una scheda unica confusa.
@@ -49,8 +92,8 @@ class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium" Qui si prepara il perimetro economico del fornitore: pagamenti, fatture, anteprime contabili e ritenute. Preventivi, ordini e fatturazione completa possono innestarsi in questa area senza mischiarsi con catalogo o assistenza.
-
-
+
+
Fornitore
@@ -261,13 +304,6 @@ class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium"
-
- Nuovo prodotto - Estrai da FE - Importa listino CSV - Internalizza asset - Prepara link Amazon -
@@ -276,13 +312,7 @@ class="inline-flex items-center rounded-full px-4 py-2 text-sm font-medium"
Catalogo operativo interno
Anagrafica prodotti/servizi del fornitore. I riferimenti esterni importati restano interni al gestionale e la pubblicazione usa il codice interno del catalogo.
-
- Nuovo prodotto - Estrai da FE - Importa CSV - Asset interni - Amazon -
+
Le azioni operative sono state raccolte sopra.
@@ -457,7 +487,7 @@ class="w-full rounded-lg border-gray-300 text-sm"
-
+
@php $features = is_array($box['fornitore_features'] ?? null) ? $box['fornitore_features'] : []; $acqua = is_array($features['acqua'] ?? null) ? $features['acqua'] : []; @@ -537,17 +567,7 @@ class="w-full rounded-lg border-gray-300 text-sm"
-
- - Impostazioni FE - - - Configura acqua - - - Scansiona FE acqua - -
+
Le azioni FE/acqua sono state raccolte nella toolbar in alto.