Compare commits

..

No commits in common. "2639c0a0e3595caa5bf9d1938236dd3b7669ea76" and "16cbec01a0f8af47e884795fc3193785af9bb1b6" have entirely different histories.

16 changed files with 235 additions and 545 deletions

View File

@ -69,18 +69,6 @@ public function mount(): void
$scope = strtolower(trim((string) request()->query('scope', 'active'))); $scope = strtolower(trim((string) request()->query('scope', 'active')));
$this->archivioScope = in_array($scope, ['active', 'all'], true) ? $scope : 'active'; $this->archivioScope = in_array($scope, ['active', 'all'], true) ? $scope : 'active';
$user = Auth::user();
if ($this->servizioFilter && $user instanceof User) {
$serviceStabileId = StabileServizio::query()
->whereKey((int) $this->servizioFilter)
->value('stabile_id');
$activeStabileId = StabileContext::resolveActiveStabileId($user);
if (is_numeric($serviceStabileId) && (int) $serviceStabileId > 0 && (int) $activeStabileId > 0 && (int) $serviceStabileId !== (int) $activeStabileId) {
$this->archivioScope = 'all';
}
}
$this->mountInteractsWithTable(); $this->mountInteractsWithTable();
} }

View File

@ -177,9 +177,9 @@ protected function getTableQuery(): Builder
return TabellaMillesimale::query()->whereRaw('1 = 0'); return TabellaMillesimale::query()->whereRaw('1 = 0');
} }
$orderColumn = Schema::hasColumn('tabelle_millesimali', 'nord') $orderColumn = Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione')
? 'nord' ? 'ordine_visualizzazione'
: (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : (Schema::hasColumn('tabelle_millesimali', 'ordinamento') ? 'ordinamento' : 'id')); : (Schema::hasColumn('tabelle_millesimali', 'nord') ? 'nord' : (Schema::hasColumn('tabelle_millesimali', 'ordinamento') ? 'ordinamento' : 'id'));
return TabellaMillesimale::query() return TabellaMillesimale::query()
->where('stabile_id', $activeStabileId) ->where('stabile_id', $activeStabileId)
@ -326,13 +326,13 @@ public function table(Table $table): Table
]) ])
->columns([ ->columns([
TextColumn::make( TextColumn::make(
Schema::hasColumn('tabelle_millesimali', 'nord') Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione')
? 'nord' ? 'ordine_visualizzazione'
: (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : 'ordinamento') : 'ordinamento'
) )
->label('Nord') ->label('Ord.')
->sortable() ->sortable()
->toggleable(), ->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('codice_tabella') TextColumn::make('codice_tabella')
->label('Codice') ->label('Codice')
@ -614,7 +614,7 @@ protected function loadTabelle(): void
->ordinato(); ->ordinato();
$this->tabelle = $q $this->tabelle = $q
->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione', 'tipo_tabella', 'tipo_calcolo', 'totale_millesimi', 'ordinamento', 'ordine_visualizzazione', 'nord', 'meta_legacy']) ->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione', 'tipo_tabella', 'tipo_calcolo', 'totale_millesimi', 'ordinamento', 'meta_legacy'])
->map(function (TabellaMillesimale $t) { ->map(function (TabellaMillesimale $t) {
$codice = $t->codice_tabella ?: ($t->nome_tabella ?: ('TAB ' . $t->id)); $codice = $t->codice_tabella ?: ($t->nome_tabella ?: ('TAB ' . $t->id));
$nome = $t->denominazione ?: ($t->nome_tabella_millesimale ?: ($t->nome_tabella ?: 'Tabella')); $nome = $t->denominazione ?: ($t->nome_tabella_millesimale ?: ($t->nome_tabella ?: 'Tabella'));
@ -634,12 +634,11 @@ protected function loadTabelle(): void
'id' => (int) $t->id, 'id' => (int) $t->id,
'codice' => $codice, 'codice' => $codice,
'nome' => $nome, 'nome' => $nome,
'nord' => $this->resolveNordValue($t),
'tipo' => $tipoLabel, 'tipo' => $tipoLabel,
'calcolo' => $calcoloLabel, 'calcolo' => $calcoloLabel,
'is_consumo' => $isConsumo, 'is_consumo' => $isConsumo,
'totale' => is_numeric($t->totale_millesimi) ? (float) $t->totale_millesimi : null, 'totale' => is_numeric($t->totale_millesimi) ? (float) $t->totale_millesimi : null,
'ordinamento' => $this->resolveNordValue($t) ?? (int) ($t->ordine_visualizzazione ?? $t->ordinamento ?? 999999), 'ordinamento' => (int) ($t->ordinamento ?? 999999),
'is_bilanciata' => (bool) ($t->is_bilanciata ?? false), 'is_bilanciata' => (bool) ($t->is_bilanciata ?? false),
'preventivo' => is_numeric($prev) ? (float) $prev : null, 'preventivo' => is_numeric($prev) ? (float) $prev : null,
'consuntivo' => is_numeric($cons) ? (float) $cons : null, 'consuntivo' => is_numeric($cons) ? (float) $cons : null,
@ -647,23 +646,6 @@ protected function loadTabelle(): void
}) })
->values() ->values()
->all(); ->all();
usort($this->tabelle, static function (array $left, array $right): int {
$leftNord = is_numeric($left['ordinamento'] ?? null) ? (int) $left['ordinamento'] : 999999;
$rightNord = is_numeric($right['ordinamento'] ?? null) ? (int) $right['ordinamento'] : 999999;
if ($leftNord !== $rightNord) {
return $leftNord <=> $rightNord;
}
$leftTipo = (string) ($left['tipo'] ?? '');
$rightTipo = (string) ($right['tipo'] ?? '');
if ($leftTipo !== $rightTipo) {
return strnatcasecmp($leftTipo, $rightTipo);
}
return strnatcasecmp((string) ($left['codice'] ?? ''), (string) ($right['codice'] ?? ''));
});
} }
protected function pickTabellaId(?int $candidate): ?int protected function pickTabellaId(?int $candidate): ?int
@ -1093,6 +1075,7 @@ private function formatCodiceUnita(?string $codice): ?string
return null; return null;
} }
$prefix = is_string($this->codiceStabile) ? trim($this->codiceStabile) : '';
if ($prefix !== '') { if ($prefix !== '') {
$pattern = '/^' . preg_quote($prefix, '/') . '[\s\-\/]+/'; $pattern = '/^' . preg_quote($prefix, '/') . '[\s\-\/]+/';
$codice = preg_replace($pattern, '', $codice) ?? $codice; $codice = preg_replace($pattern, '', $codice) ?? $codice;
@ -1355,16 +1338,4 @@ private function isConsumoCalcolo(?string $calcolo): bool
$c = strtolower(trim((string) ($calcolo ?? ''))); $c = strtolower(trim((string) ($calcolo ?? '')));
return in_array($c, ['acqua', 'a', 'consumo', 'c', 'a_consumo'], true); return in_array($c, ['acqua', 'a', 'consumo', 'c', 'a_consumo'], true);
} }
private function resolveNordValue(TabellaMillesimale $t): ?int
{
foreach (['nord', 'ordine_visualizzazione', 'ordinamento'] as $field) {
$value = $t->{$field} ?? null;
if (is_numeric($value)) {
return (int) $value;
}
}
return null;
}
} }

View File

@ -66,32 +66,6 @@ public function getActiveStabileLabelProperty(): string
return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) $stabile->id); return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) $stabile->id);
} }
public function getActiveStabileDestinationSummaryProperty(): string
{
$user = Auth::user();
if (! $user instanceof User) {
return 'Stabile non selezionato';
}
$stabile = StabileContext::getActiveStabile($user);
if (! $stabile instanceof Stabile) {
return 'Stabile non selezionato';
}
$summary = $this->getActiveStabileLabelProperty();
$cf = $this->resolveStabileSoggettoCf($stabile);
if ($cf !== '') {
$summary .= "\nCodice fiscale soggetto usato per lo scarico: {$cf}";
}
if ($this->isLikelyTechnicalFeShell($stabile)) {
$summary .= "\nATTENZIONE: lo stabile attivo sembra uno shell tecnico FE separato. Per scaricare le fatture del condominio cambia lo stabile in topbar prima di procedere.";
}
return $summary;
}
private function safeUtf8(string $value): string private function safeUtf8(string $value): string
{ {
$value = preg_replace('/^\xEF\xBB\xBF/', '', $value) ?? $value; $value = preg_replace('/^\xEF\xBB\xBF/', '', $value) ?? $value;
@ -256,12 +230,6 @@ public function downloadCassettoQuarter(string $quarter, string $dal, string $al
return; return;
} }
if ($this->isLikelyTechnicalFeShell($stabile)) {
$this->notifyTechnicalFeShellBlocked();
$this->quarterDownloads[$key] = false;
return;
}
$amministratoreId = (int) ($stabile->amministratore_id ?? 0); $amministratoreId = (int) ($stabile->amministratore_id ?? 0);
if ($amministratoreId <= 0) { if ($amministratoreId <= 0) {
Notification::make()->title('Amministratore non valido')->danger()->send(); Notification::make()->title('Amministratore non valido')->danger()->send();
@ -272,7 +240,7 @@ public function downloadCassettoQuarter(string $quarter, string $dal, string $al
if ($this->isCassettoQuarterCompleteFor($amministratoreId, (int) $stabile->id, $dal, $al)) { if ($this->isCassettoQuarterCompleteFor($amministratoreId, (int) $stabile->id, $dal, $al)) {
Notification::make() Notification::make()
->title('Trimestre già completo') ->title('Trimestre già completo')
->body($this->safeUtf8("{$quarter} {$anno} risulta già scaricato (COMPLETO). Per rieseguire lo stesso periodo usa 'Scarica da Cassetto Fiscale' e abilita 'Reimport / completa' oppure 'Non saltare'.")) ->body($this->safeUtf8("{$quarter} {$anno} risulta già scaricato (COMPLETO)."))
->warning() ->warning()
->send(); ->send();
return; return;
@ -337,44 +305,6 @@ private function isCassettoQuarterCompleteFor(int $amministratoreId, int $stabil
return $errors === 0 && $filesTotal > 0 && ($imported + $duplicates) >= $filesTotal; return $errors === 0 && $filesTotal > 0 && ($imported + $duplicates) >= $filesTotal;
} }
private function resolveStabileSoggettoCf(Stabile $stabile): string
{
$cf = trim((string) ($stabile->codice_fiscale ?? ''));
if ($cf === '') {
try {
$stabile->loadMissing('rubrica');
$cf = trim((string) ($stabile->rubrica?->codice_fiscale ?? ''));
} catch (\Throwable) {
// ignore
}
}
return strtoupper(trim($cf));
}
private function isLikelyTechnicalFeShell(Stabile $stabile): bool
{
$stabileCf = $this->resolveStabileSoggettoCf($stabile);
$adminCf = strtoupper(trim((string) ($stabile->amministratore?->codice_fiscale_studio ?? '')));
$code = strtoupper(trim((string) ($stabile->codice_stabile ?? '')));
$legacyCode = trim((string) ($stabile->cod_stabile ?? ''));
return $stabileCf !== ''
&& $adminCf !== ''
&& $stabileCf === $adminCf
&& (str_starts_with($code, 'FE') || $legacyCode === '');
}
private function notifyTechnicalFeShellBlocked(): void
{
Notification::make()
->title('Stabile tecnico FE separato')
->body('Lo scarico è bloccato perché lo stabile attivo usa il codice fiscale dello studio/amministratore. Se devi scaricare le fatture del condominio, cambia lo stabile attivo in topbar e seleziona il condominio reale.')
->warning()
->send();
}
public function getArchivioRigheVisteCount(): int public function getArchivioRigheVisteCount(): int
{ {
$records = $this->getTableRecords(); $records = $this->getTableRecords();
@ -672,7 +602,7 @@ protected function getHeaderActions(): array
->form([ ->form([
Placeholder::make('stabile_attivo_info') Placeholder::make('stabile_attivo_info')
->label('Stabile di destinazione') ->label('Stabile di destinazione')
->content(fn(): string => $this->getActiveStabileDestinationSummaryProperty()), ->content(fn(): string => $this->getActiveStabileLabelProperty()),
Select::make('anno') Select::make('anno')
->label('Anno') ->label('Anno')
@ -735,9 +665,6 @@ protected function getHeaderActions(): array
Toggle::make('force_update') Toggle::make('force_update')
->label('Reimport / completa anche se già presente') ->label('Reimport / completa anche se già presente')
->default(false), ->default(false),
Toggle::make('no_skip')
->label('Non saltare se lo stesso periodo ha già un log OK')
->default(false),
Select::make('importa_righe') Select::make('importa_righe')
->label('Import righe') ->label('Import righe')
->options([ ->options([
@ -764,11 +691,6 @@ protected function getHeaderActions(): array
return; return;
} }
if ($this->isLikelyTechnicalFeShell($stabile)) {
$this->notifyTechnicalFeShellBlocked();
return;
}
$amministratore = $stabile->amministratore; $amministratore = $stabile->amministratore;
if (! $amministratore) { if (! $amministratore) {
Notification::make()->title('Amministratore non trovato')->danger()->send(); Notification::make()->title('Amministratore non trovato')->danger()->send();
@ -785,13 +707,11 @@ protected function getHeaderActions(): array
} }
$forceUpdate = (bool) ($data['force_update'] ?? false); $forceUpdate = (bool) ($data['force_update'] ?? false);
$noSkip = (bool) ($data['no_skip'] ?? false);
$allowRerun = $forceUpdate || $noSkip;
// Blocco trimestri: se il trimestre è già completo, evita ri-scarico (salvo force_update). // Blocco trimestri: se il trimestre è già completo, evita ri-scarico (salvo force_update).
$periodo = (string) ($data['periodo'] ?? ''); $periodo = (string) ($data['periodo'] ?? '');
$anno = is_numeric($data['anno'] ?? null) ? (int) $data['anno'] : (int) $dal->format('Y'); $anno = is_numeric($data['anno'] ?? null) ? (int) $data['anno'] : (int) $dal->format('Y');
if (! $allowRerun && in_array($periodo, ['Q1', 'Q2', 'Q3', 'Q4'], true)) { if (! $forceUpdate && in_array($periodo, ['Q1', 'Q2', 'Q3', 'Q4'], true)) {
[$expectedDal, $expectedAl] = match ($periodo) { [$expectedDal, $expectedAl] = match ($periodo) {
'Q2' => [$anno . '-04-01', $anno . '-06-30'], 'Q2' => [$anno . '-04-01', $anno . '-06-30'],
'Q3' => [$anno . '-07-01', $anno . '-09-30'], 'Q3' => [$anno . '-07-01', $anno . '-09-30'],
@ -812,7 +732,7 @@ protected function getHeaderActions(): array
} }
// Anti-duplicati veloce: se esiste già un OK per lo stesso periodo, non avviamo niente. // Anti-duplicati veloce: se esiste già un OK per lo stesso periodo, non avviamo niente.
if (! $allowRerun) { if (! $forceUpdate) {
$cf = trim((string) ($stabile->codice_fiscale ?? '')); $cf = trim((string) ($stabile->codice_fiscale ?? ''));
if ($cf === '') { if ($cf === '') {
try { try {
@ -877,7 +797,6 @@ protected function getHeaderActions(): array
(int) $user->id, (int) $user->id,
[ [
'force_update' => (bool) ($data['force_update'] ?? false), 'force_update' => (bool) ($data['force_update'] ?? false),
'no_skip' => (bool) ($data['no_skip'] ?? false),
'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'), 'importa_righe' => (string) ($data['importa_righe'] ?? 'auto'),
'create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? true), 'create_fornitore_if_missing' => (bool) ($data['create_fornitore_if_missing'] ?? true),
], ],
@ -891,7 +810,7 @@ protected function getHeaderActions(): array
Notification::make() Notification::make()
->title('Scarico avviato') ->title('Scarico avviato')
->body("Operazione avviata in background. Se hai abilitato 'Reimport / completa' o 'Non saltare', lo stesso periodo verrà rieseguito.\nAggiorna tra poco e controlla la sezione 'Ultimi scarichi'.") ->body("Operazione avviata in background. Se già scaricato, verrà saltato automaticamente.\nAggiorna tra poco e controlla la sezione 'Ultimi scarichi'.")
->success() ->success()
->send(); ->send();
}), }),

View File

@ -128,24 +128,6 @@ public function updatedUnitaId(): void
$this->syncSuggestedReaderName(); $this->syncSuggestedReaderName();
} }
public function updatedUnitaSearch(): void
{
$needle = trim($this->unitaSearch);
if ($needle === '') {
return;
}
$filtered = $this->getFilteredUnitaOptionsProperty();
if (count($filtered) === 1) {
$this->applySelectedUnita((int) ($filtered[0]['id'] ?? 0));
}
}
public function selectUnita(int $unitaId): void
{
$this->applySelectedUnita($unitaId, true);
}
public function updatedPendingWaterPhotos(): void public function updatedPendingWaterPhotos(): void
{ {
$this->appendPendingWaterPhotos(); $this->appendPendingWaterPhotos();
@ -585,14 +567,6 @@ public function getFilteredUnitaOptionsProperty(): array
return array_values(array_filter($this->unitaOptions, fn(array $option): bool => str_contains((string) ($option['search_text'] ?? ''), $needle))); return array_values(array_filter($this->unitaOptions, fn(array $option): bool => str_contains((string) ($option['search_text'] ?? ''), $needle)));
} }
/**
* @return array<int,array{id:int,label:string,reader_label:string,folder_prefix:string,search_text:string}>
*/
public function getLightUnitaSearchResultsProperty(): array
{
return array_slice($this->getFilteredUnitaOptionsProperty(), 0, 10);
}
private function loadPreviousReading(): void private function loadPreviousReading(): void
{ {
$this->previousReading = null; $this->previousReading = null;
@ -901,7 +875,12 @@ private function resolveUnitReaderLabel(UnitaImmobiliare $unita): string
} }
} }
$historicLabel = $this->resolveRelevantHistoricNames($unita)[0] ?? ''; $historic = $unita->nominativiStorici
?->sortByDesc(fn($item) => (string) ($item->data_fine ?? '9999-12-31'))
->sortByDesc(fn($item) => (string) ($item->data_inizio ?? ''))
->first();
$historicLabel = trim((string) ($historic->nominativo ?? ''));
if ($historicLabel !== '') { if ($historicLabel !== '') {
return $historicLabel; return $historicLabel;
} }
@ -928,7 +907,12 @@ private function resolveUnitSearchNames(UnitaImmobiliare $unita): array
} }
} }
$names = array_merge($names, $this->resolveRelevantHistoricNames($unita)); foreach (($unita->nominativiStorici ?? collect()) as $historic) {
$label = trim((string) ($historic->nominativo ?? ''));
if ($label !== '') {
$names[] = $label;
}
}
$fallback = trim((string) ($unita->denominazione ?? '')); $fallback = trim((string) ($unita->denominazione ?? ''));
if ($fallback !== '') { if ($fallback !== '') {
@ -938,70 +922,6 @@ private function resolveUnitSearchNames(UnitaImmobiliare $unita): array
return array_values(array_unique(array_filter($names))); return array_values(array_unique(array_filter($names)));
} }
/**
* @return array<int,string>
*/
private function resolveRelevantHistoricNames(UnitaImmobiliare $unita): array
{
$today = now()->toDateString();
$historic = collect($unita->nominativiStorici ?? []);
$current = $historic
->filter(function ($item) use ($today): bool {
$start = trim((string) ($item->data_inizio ?? ''));
$end = trim((string) ($item->data_fine ?? ''));
return ($start === '' || $start <= $today)
&& ($end === '' || $end >= $today);
})
->map(fn($item): string => trim((string) ($item->nominativo ?? '')))
->filter()
->unique()
->values()
->all();
if ($current !== []) {
return $current;
}
return $historic
->sortByDesc(fn($item): string => sprintf(
'%s|%s|%010d',
(string) ($item->data_fine ?? '9999-12-31'),
(string) ($item->data_inizio ?? ''),
(int) ($item->id ?? 0)
))
->map(fn($item): string => trim((string) ($item->nominativo ?? '')))
->filter()
->unique()
->take(1)
->values()
->all();
}
private function applySelectedUnita(int $unitaId, bool $syncSearchLabel = false): void
{
if ($unitaId <= 0) {
return;
}
foreach ($this->unitaOptions as $option) {
if ((int) ($option['id'] ?? 0) !== $unitaId) {
continue;
}
$this->unitaId = $unitaId;
if ($syncSearchLabel) {
$this->unitaSearch = (string) ($option['label'] ?? '');
}
$this->loadPreviousReading();
$this->syncSuggestedReaderName();
return;
}
}
private function buildUnitNaturalSortKey(UnitaImmobiliare $unita, string $readerLabel): string private function buildUnitNaturalSortKey(UnitaImmobiliare $unita, string $readerLabel): string
{ {
$parts = array_filter([ $parts = array_filter([

View File

@ -1,9 +1,7 @@
<?php <?php
namespace App\Filament\Pages\Supporto; namespace App\Filament\Pages\Supporto;
use App\Models\User;
use BackedEnum; use BackedEnum;
use Illuminate\Support\Facades\Auth;
use UnitEnum; use UnitEnum;
class TicketAcquaLight extends TicketAcqua class TicketAcquaLight extends TicketAcqua
@ -21,32 +19,4 @@ class TicketAcquaLight extends TicketAcqua
protected static ?string $slug = 'supporto/ticket-acqua-light'; protected static ?string $slug = 'supporto/ticket-acqua-light';
protected string $view = 'filament.pages.supporto.ticket-acqua-light'; protected string $view = 'filament.pages.supporto.ticket-acqua-light';
public function mount(): void
{
parent::mount();
$this->resetLightUnitSelection();
}
public function updatedStabileId(): void
{
parent::updatedStabileId();
$this->resetLightUnitSelection();
}
private function resetLightUnitSelection(): void
{
$user = Auth::user();
$this->unitaId = null;
$this->unitaSearch = '';
$this->previousReading = null;
$this->suggestedReaderName = null;
if ($user instanceof User) {
$this->rilevatoreNome = trim((string) $user->name) ?: $this->rilevatoreNome;
}
}
} }

View File

@ -51,6 +51,7 @@ public function panel(Panel $panel): Panel
->renderHook(PanelsRenderHook::HEAD_END, fn() => view('filament.components.legacy-admin-assets')) ->renderHook(PanelsRenderHook::HEAD_END, fn() => view('filament.components.legacy-admin-assets'))
->renderHook(PanelsRenderHook::TOPBAR_LOGO_AFTER, fn() => view('filament.components.topbar-context')) ->renderHook(PanelsRenderHook::TOPBAR_LOGO_AFTER, fn() => view('filament.components.topbar-context'))
->renderHook(PanelsRenderHook::USER_MENU_BEFORE, fn() => view('filament.components.topbar-icons')) ->renderHook(PanelsRenderHook::USER_MENU_BEFORE, fn() => view('filament.components.topbar-icons'))
->renderHook(PanelsRenderHook::FOOTER, fn() => view('filament.components.footer'))
->navigationItems([ ->navigationItems([
NavigationItem::make('Rubrica Unica') NavigationItem::make('Rubrica Unica')
->group('NetGescon') ->group('NetGescon')
@ -132,7 +133,7 @@ public function panel(Panel $panel): Panel
]); ]);
} }
protected function resolveSupplierCatalogUrl(): string private function resolveSupplierCatalogUrl(): string
{ {
$user = Auth::user(); $user = Auth::user();

View File

@ -1,4 +1,5 @@
<?php <?php
namespace App\Services\Consumi; namespace App\Services\Consumi;
use App\Models\CategoriaTicket; use App\Models\CategoriaTicket;
@ -31,25 +32,6 @@ public function ingest(
return ['status' => 'error', 'message' => 'Stabile non disponibile']; return ['status' => 'error', 'message' => 'Stabile non disponibile'];
} }
$storedPayload = [];
$storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : '';
if ($storedRaw !== '') {
$decoded = json_decode($storedRaw, true);
if (is_array($decoded) && (($decoded['type'] ?? null) === 'acqua')) {
$storedPayload = $decoded;
}
}
if ((empty($parsed['codici']) || ! is_array($parsed['codici'])) && is_array($storedPayload['codici'] ?? null)) {
$parsed['codici'] = $storedPayload['codici'];
}
if ((empty($parsed['contatore']) || ! is_array($parsed['contatore'])) && is_array($storedPayload['contatore'] ?? null)) {
$parsed['contatore'] = $storedPayload['contatore'];
}
if ((empty($parsed['consumi']) || ! is_array($parsed['consumi'])) && is_array($storedPayload['consumi'] ?? null)) {
$parsed['consumi'] = $storedPayload['consumi'];
}
$codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : []; $codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [];
$contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : []; $contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [];
$consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []; $consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : [];

View File

@ -1,73 +1,3 @@
# SESSION HANDOFF - 2026-04-13 / 2026-04-14
## Stato operativo reale alla chiusura sessione
- Repository: `/home/michele/netgescon/netgescon-day0`
- Focus stabile utente: `0021`, con attenzione anche allo shell tecnico FE `FE221005` (`stabili.id=29`)
- Obiettivo operativo chiuso in questa sessione:
- rendere esplicito e sicuro il CF soggetto nello scarico FE
- semplificare il flusso `ticket-acqua-light`
- rimuovere il footer Filament invasivo su mobile
- riallineare pagina `tabelle-millesimali` a `nord`, `tipo`, `calcolo`
- eliminare i blocchi Blade fragili nelle viste `ticket-acqua` e `ticket-acqua-generale`
## Verifiche fatte e confermate
- `php artisan view:clear && php artisan view:cache` eseguito con esito OK dopo gli ultimi fix.
- `php artisan route:list` carica `755` route.
- Il service FE usa il CF dello stabile, non il CF studio dell'amministratore:
- `stabili.id=25 / codice_stabile=0021 / CF=97487690584`
- `stabili.id=29 / codice_stabile=FE221005 / CF=10055221005`
- Il problema operativo dello scarico FE nasceva dal contesto dello stabile attivo in topbar/sessione, non dalla chiamata HTTP finale.
- Nella pagina FE è stato aggiunto un riepilogo esplicito del `Codice fiscale soggetto usato per lo scarico`.
- Le due Blade acqua sono state semplificate spostando il rendering metadati foto in partial condivisa, per evitare parse error residui in staging dovuti a blocchi Blade annidati.
## File toccati in questa fase
- `app/Providers/Filament/AdminFilamentPanelProvider.php`
- chain Filament riparata
- footer hook rimosso dal panel
- `app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php`
- preferenza a `tabelle_millesimali.nord` per ordinamento archivio
- `nord` portato nel payload usato dalla Blade
- lista tabelle ordinata per `nord`, poi `tipo`, poi codice
- `resources/views/filament/pages/condomini/tabelle-millesimali.blade.php`
- badge visivo `Ord. {nord}`
- `tipo` e `calcolo` mantenuti visibili nelle liste `prospetto` e `voci`
- `resources/views/filament/pages/supporto/ticket-acqua.blade.php`
- `resources/views/filament/pages/supporto/ticket-acqua-generale.blade.php`
- blocchi metadati foto sostituiti con partial condivisa
- `resources/views/filament/pages/supporto/partials/photo-metadata.blade.php`
- nuova partial minima in PHP/HTML, senza direttive Blade annidate
## Comandi rapidi per ripartenza domani
```bash
cd /home/michele/netgescon/netgescon-day0
git status --short
php artisan view:clear
php artisan view:cache
php artisan route:list | tail -n 2
php artisan netgescon:qa-millesimi-spese --stabile=0021 --show-missing=20
tail -n 120 storage/logs/laravel.log
```
## Punti ancora aperti da riprendere
1. QA millesimi/voci da rendere piu accurato per le gestioni straordinarie legate all'anno/esercizio.
2. Validazione staging reale dopo `git push` + update staging:
- `/admin-filament/condomini/tabelle-millesimali`
- `/admin-filament/supporto/ticket-acqua`
- `/admin-filament/supporto/ticket-acqua-generale`
3. Controllo del bug `fornitore Livewire root` ancora aperto nel tracker.
4. Diagnosi separata dei nominativi rumorosi per `0023`.
## Prompt consigliato per un altro agent
"Riparti da `docs/ai/SESSION_HANDOFF.md`. Prima verifica `git status`, poi esegui `php artisan view:clear && php artisan view:cache`. Controlla staging su `tabelle-millesimali`, `ticket-acqua` e `ticket-acqua-generale`. Se il deploy e allineato, passa al raffinamento del QA millesimi/voci per gestioni straordinarie con anno/esercizio, senza fare bonifiche contabili distruttive."
---
# SESSION HANDOFF - 2026-04-08 # SESSION HANDOFF - 2026-04-08
## Contesto operativo attuale ## Contesto operativo attuale

View File

@ -1 +1,48 @@
@php
$user = \Illuminate\Support\Facades\Auth::user();
$version = config('netgescon.version') ?? config('app.version');
@endphp
@once
<style>
/* Ensure the Filament layout behaves like a full-height flex column,
so this footer is pushed to the bottom even on short pages.
This is a safe fallback when the Vite theme CSS isn't loaded. */
.fi-main-ctn {
min-height: 100vh;
min-height: 100dvh;
height: auto !important;
display: flex !important;
flex-direction: column !important;
}
.fi-main {
flex: 1 1 auto;
min-height: 0;
height: auto !important;
}
.netgescon-footer {
flex: none;
margin-top: auto;
width: 100% !important;
position: sticky !important;
bottom: 0 !important;
z-index: 10 !important;
}
</style>
@endonce
<div class="netgescon-footer px-6 py-3 text-xs text-gray-500 border-t bg-white">
<div class="mx-auto max-w-7xl flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-wrap items-center gap-x-3 gap-y-1">
<div>NetGescon{{ $version ? ' · v' . $version : '' }}</div>
<div class="text-gray-400">|</div>
<div>{{ now()->format('d/m/Y H:i') }}</div>
</div>
<div class="flex items-center gap-3">
<a href="{{ \App\Filament\Pages\Supporto\RichiestaHelp::getUrl(panel: 'admin-filament') }}" class="text-primary-600 hover:underline">
Richiedi help / segnala bug
</a>
</div>
</div>
</div>

View File

@ -70,14 +70,9 @@ class="block rounded-xl border p-3 transition
> >
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div> <div>
<div class="flex flex-wrap items-center gap-2">
<div class="text-sm font-semibold {{ $selected ? 'text-primary-700' : 'text-gray-900' }}"> <div class="text-sm font-semibold {{ $selected ? 'text-primary-700' : 'text-gray-900' }}">
{{ $t['codice'] }} {{ $t['codice'] }}
</div> </div>
@if(isset($t['nord']) && $t['nord'] !== null)
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-700">Ord. {{ $t['nord'] }}</span>
@endif
</div>
<div class="text-xs text-gray-600">{{ $t['nome'] }}</div> <div class="text-xs text-gray-600">{{ $t['nome'] }}</div>
@if(!empty($t['tipo']) || !empty($t['calcolo'])) @if(!empty($t['tipo']) || !empty($t['calcolo']))
<div class="mt-1 text-xs text-gray-500"> <div class="mt-1 text-xs text-gray-500">
@ -246,14 +241,9 @@ class="block rounded-xl border p-3 transition
> >
<div class="flex items-start justify-between gap-3"> <div class="flex items-start justify-between gap-3">
<div> <div>
<div class="flex flex-wrap items-center gap-2">
<div class="text-sm font-semibold {{ $selected ? 'text-primary-700' : 'text-gray-900' }}"> <div class="text-sm font-semibold {{ $selected ? 'text-primary-700' : 'text-gray-900' }}">
{{ $t['codice'] }} {{ $t['codice'] }}
</div> </div>
@if(isset($t['nord']) && $t['nord'] !== null)
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-700">Ord. {{ $t['nord'] }}</span>
@endif
</div>
<div class="text-xs text-gray-600">{{ $t['nome'] }}</div> <div class="text-xs text-gray-600">{{ $t['nome'] }}</div>
@if(!empty($t['tipo']) || !empty($t['calcolo'])) @if(!empty($t['tipo']) || !empty($t['calcolo']))
<div class="mt-1 text-xs text-gray-500"> <div class="mt-1 text-xs text-gray-500">

View File

@ -1,20 +0,0 @@
<?php
$badgeRows = is_array($badgeRows ?? null) ? $badgeRows : [];
$detailRows = is_array($detailRows ?? null) ? $detailRows : [];
?>
<?php if ($badgeRows !== []): ?>
<div class="mt-2 flex flex-wrap gap-1">
<?php foreach ($badgeRows as $badge): ?>
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold <?= e((string) ($badge['class'] ?? '')) ?>"><?= e((string) ($badge['label'] ?? '')) ?></span>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($detailRows !== []): ?>
<div class="mt-2 rounded-lg border border-slate-200 bg-white p-2 text-[11px] text-slate-700">
<?php foreach ($detailRows as $detail): ?>
<div><span class="font-medium"><?= e((string) ($detail['label'] ?? '')) ?>:</span> <?= e((string) ($detail['value'] ?? '')) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>

View File

@ -63,9 +63,7 @@
</div> </div>
</div> </div>
@php @php($service = $this->selectedServiceSnapshot)
$service = $this->selectedServiceSnapshot;
@endphp
@if($service) @if($service)
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 shadow-sm"> <div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 shadow-sm">
<div class="text-sm font-semibold text-emerald-900">Riferimenti contratto selezionato</div> <div class="text-sm font-semibold text-emerald-900">Riferimenti contratto selezionato</div>
@ -334,7 +332,13 @@
]; ];
} }
@endphp @endphp
@include('filament.pages.supporto.partials.photo-metadata', ['badgeRows' => [], 'detailRows' => $detailRows]) @if($detailRows !== [])
<div class="mt-2 rounded-lg border border-slate-200 bg-white p-2 text-[11px] text-slate-700">
@foreach($detailRows as $detail)
<div><span class="font-medium">{{ $detail['label'] }}:</span> {{ $detail['value'] }}</div>
@endforeach
</div>
@endif
<label class="mt-2 block"> <label class="mt-2 block">
<span class="mb-1 block text-[11px] font-medium">Nota foto</span> <span class="mb-1 block text-[11px] font-medium">Nota foto</span>
<input type="text" wire:model.defer="generalWaterPhotoDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. display principale, targhetta matricola, quadro contatori" /> <input type="text" wire:model.defer="generalWaterPhotoDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. display principale, targhetta matricola, quadro contatori" />

View File

@ -46,44 +46,19 @@
</label> </label>
<label class="block text-sm"> <label class="block text-sm">
<span class="mb-1 block font-medium">Cerca unità / nominativo</span> <span class="mb-1 block font-medium">Cerca nominativo / unità</span>
<input type="text" wire:model.live.debounce.250ms="unitaSearch" class="w-full rounded-lg border-gray-300" placeholder="Es. Rossi, Int. 10, 0021-A-20..." /> <input type="text" wire:model.live.debounce.250ms="unitaSearch" class="w-full rounded-lg border-gray-300" placeholder="Es. Maggiore, Int. 10, B_4..." />
<div class="mt-1 text-[11px] text-slate-500">Un solo campo: cerca per codice unità, interno, piano o nominativo attuale dello stabile selezionato.</div>
</label> </label>
@php <label class="block text-sm">
$searchResults = $this->lightUnitaSearchResults; <span class="mb-1 block font-medium">Unità immobiliare</span>
$selectedUnit = collect($this->unitaOptions)->firstWhere('id', (int) ($unitaId ?? 0)); <select wire:model.live="unitaId" class="w-full rounded-lg border-gray-300">
$searchTerm = trim((string) $unitaSearch); <option value="">Seleziona unità</option>
@endphp @foreach($this->filteredUnitaOptions as $option)
<div class="rounded-2xl border border-slate-200 bg-slate-50 p-3"> <option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
@if($selectedUnit)
<div class="rounded-xl border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-900">
<div class="font-semibold">Unità selezionata</div>
<div class="mt-1">{{ $selectedUnit['label'] }}</div>
@if(filled($selectedUnit['reader_label'] ?? null))
<div class="mt-1 text-[11px] text-emerald-800">Nominativo collegato: {{ $selectedUnit['reader_label'] }}</div>
@endif
</div>
@elseif($searchTerm === '')
<div class="text-sm text-slate-500">Digita almeno parte del nominativo o del codice unità per selezionare rapidamente il destinatario corretto.</div>
@endif
@if($searchTerm !== '' && $searchResults !== [])
<div class="mt-3 space-y-2">
@foreach($searchResults as $option)
<button type="button" wire:click="selectUnita({{ (int) $option['id'] }})" class="block w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-left text-sm shadow-sm transition hover:border-sky-300 hover:bg-sky-50">
<div class="font-medium text-slate-900">{{ $option['label'] }}</div>
@if(filled($option['reader_label'] ?? null))
<div class="mt-1 text-[11px] text-slate-500">{{ $option['reader_label'] }}</div>
@endif
</button>
@endforeach @endforeach
</div> </select>
@elseif($searchTerm !== '' && $searchResults === []) </label>
<div class="mt-3 rounded-xl border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900">Nessuna unità trovata per questa ricerca nello stabile selezionato.</div>
@endif
</div>
<label class="block text-sm"> <label class="block text-sm">
<span class="mb-1 block font-medium">Nominativo letturista</span> <span class="mb-1 block font-medium">Nominativo letturista</span>

View File

@ -244,9 +244,7 @@
<div wire:loading wire:target="pendingWaterPhotos" class="mt-2 text-xs font-medium text-amber-700">Caricamento foto in corso: preparo la coda della lettura acqua.</div> <div wire:loading wire:target="pendingWaterPhotos" class="mt-2 text-xs font-medium text-amber-700">Caricamento foto in corso: preparo la coda della lettura acqua.</div>
@if(count($this->selectedWaterUploads) > 0) @if(count($this->selectedWaterUploads) > 0)
@php @php($focusUpload = $this->selectedWaterUploads[0] ?? null)
$focusUpload = $this->selectedWaterUploads[0] ?? null;
@endphp
@if($focusUpload) @if($focusUpload)
<div class="mt-4 rounded-2xl border border-sky-200 bg-sky-50 p-3"> <div class="mt-4 rounded-2xl border border-sky-200 bg-sky-50 p-3">
<div class="mb-2 text-xs font-semibold text-sky-900">Chicca lettura rapida</div> <div class="mb-2 text-xs font-semibold text-sky-900">Chicca lettura rapida</div>
@ -320,7 +318,22 @@
]; ];
} }
@endphp @endphp
@include('filament.pages.supporto.partials.photo-metadata', ['badgeRows' => $badgeRows, 'detailRows' => $detailRows]) @if($badgeRows !== [] || $detailRows !== [])
@if($badgeRows !== [])
<div class="mt-2 flex flex-wrap gap-1">
@foreach($badgeRows as $badge)
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold {{ $badge['class'] }}">{{ $badge['label'] }}</span>
@endforeach
</div>
@endif
@if($detailRows !== [])
<div class="mt-2 rounded-lg border border-slate-200 bg-white p-2 text-[11px] text-slate-700">
@foreach($detailRows as $detail)
<div><span class="font-medium">{{ $detail['label'] }}:</span> {{ $detail['value'] }}</div>
@endforeach
</div>
@endif
@endif
<label class="mt-2 block"> <label class="mt-2 block">
<span class="mb-1 block text-[11px] font-medium">Nota sotto la foto</span> <span class="mb-1 block text-[11px] font-medium">Nota sotto la foto</span>
<input type="text" wire:model.defer="waterPhotoDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. contatore cucina, cifra poco leggibile" /> <input type="text" wire:model.defer="waterPhotoDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. contatore cucina, cifra poco leggibile" />