From fe9e8adb725638533ba8bb8ff335800704242dfd Mon Sep 17 00:00:00 2001 From: michele Date: Tue, 14 Apr 2026 10:14:42 +0000 Subject: [PATCH] Refine letture UI and auto-link FE water readings --- .../Condomini/LettureServiziArchivio.php | 198 +++++++++++++++--- .../FattureElettronicheP7mRicevute.php | 188 ++++++++++++++++- docs/ai/SESSION_HANDOFF.md | 21 ++ .../lettura-servizio-foto-modal.blade.php | 46 ++++ 4 files changed, 423 insertions(+), 30 deletions(-) create mode 100644 resources/views/filament/pages/condomini/partials/lettura-servizio-foto-modal.blade.php diff --git a/app/Filament/Pages/Condomini/LettureServiziArchivio.php b/app/Filament/Pages/Condomini/LettureServiziArchivio.php index b5976d5..b141442 100644 --- a/app/Filament/Pages/Condomini/LettureServiziArchivio.php +++ b/app/Filament/Pages/Condomini/LettureServiziArchivio.php @@ -20,11 +20,13 @@ use Filament\Forms\Components\TextInput; use Filament\Notifications\Notification; use Filament\Pages\Page; +use Filament\Tables\Columns\ImageColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Concerns\InteractsWithTable; use Filament\Tables\Contracts\HasTable; use Filament\Tables\Filters\SelectFilter; use Filament\Tables\Table; +use Illuminate\Contracts\View\View; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; @@ -224,8 +226,17 @@ public function table(Table $table): Table }), ]) ->columns([ - TextColumn::make('periodo_dal')->label('Dal')->date('d/m/Y')->sortable()->toggleable(), - TextColumn::make('periodo_al')->label('Al')->date('d/m/Y')->sortable(), + ImageColumn::make('miniatura_foto') + ->label('Foto') + ->getStateUsing(fn(StabileServizioLettura $record): ?string => $this->resolveReadingPrimaryPhotoPath($record)) + ->disk('public') + ->square() + ->size(42) + ->toggleable() + ->extraAttributes(['class' => 'py-2']), + + TextColumn::make('periodo_dal')->label('Dal')->date('d/m/Y')->sortable()->toggleable()->extraAttributes(['class' => 'text-[11px] leading-tight']), + TextColumn::make('periodo_al')->label('Al')->date('d/m/Y')->sortable()->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('stabile.denominazione') ->label('Stabile') @@ -240,7 +251,19 @@ public function table(Table $table): Table return trim($codice . ($nome !== '' ? (' - ' . $nome) : '')) ?: ('Stabile #' . $stabile->id); }) - ->wrap() + ->limit(36) + ->tooltip(function (StabileServizioLettura $record): string { + $stabile = $record->stabile; + if (! $stabile instanceof Stabile) { + return '—'; + } + + $codice = trim((string) ($stabile->codice_stabile ?? '')); + $nome = trim((string) ($stabile->denominazione ?? '')); + + return trim($codice . ($nome !== '' ? (' - ' . $nome) : '')) ?: ('Stabile #' . $stabile->id); + }) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: $this->archivioScope !== 'all'), TextColumn::make('servizio.nome') @@ -254,23 +277,37 @@ public function table(Table $table): Table $tipo = trim((string) ($record->servizio?->tipo ?? '')); return $tipo !== '' ? strtoupper($tipo) : '—'; }) - ->wrap() + ->limit(30) + ->tooltip(function (StabileServizioLettura $record): string { + $nome = trim((string) ($record->servizio?->nome ?? '')); + if ($nome !== '') { + return $nome; + } + + $tipo = trim((string) ($record->servizio?->tipo ?? '')); + return $tipo !== '' ? strtoupper($tipo) : '—'; + }) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), - TextColumn::make('servizio.contatore_matricola')->label('Matricola')->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('servizio.contatore_matricola')->label('Matricola')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('unitaImmobiliare.denominazione') ->label('Attribuzione') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { return $this->resolveReadingAssignmentLabel($record); }) - ->wrap() + ->limit(34) + ->tooltip(fn(StabileServizioLettura $record): string => $this->resolveReadingAssignmentLabel($record)) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('rilevatore_nome') ->label('Letturista / nominativo') ->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—') - ->wrap() + ->limit(28) + ->tooltip(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('consumo_valore') @@ -284,13 +321,15 @@ public function table(Table $table): Table $unit = trim((string) ($record->consumo_unita ?? '')); return trim((string) $val) . ($unit !== '' ? (' ' . $unit) : ''); }) - ->sortable(), + ->sortable() + ->extraAttributes(['class' => 'text-[11px] leading-tight font-medium']), - TextColumn::make('importo_totale')->label('Importo')->money('EUR')->sortable()->toggleable(), + TextColumn::make('importo_totale')->label('Importo')->money('EUR')->sortable()->toggleable()->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('tipologia_lettura') ->label('Tipo lettura') ->formatStateUsing(fn($state) => $tipologiaOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—')) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('canale_acquisizione') @@ -307,12 +346,14 @@ public function table(Table $table): Table default => $v !== '' ? strtoupper($v) : '—', }; }) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('workflow_stato') ->label('Workflow') ->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? (string) $state : 'Acquisita') ->badge() + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('raw.consistency_check.summary') @@ -329,7 +370,12 @@ public function table(Table $table): Table default => 'gray', }; }) - ->wrap() + ->limit(28) + ->tooltip(function (StabileServizioLettura $record): string { + $summary = trim((string) data_get($record->raw, 'consistency_check.summary', '')); + return $summary !== '' ? $summary : '—'; + }) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('lettura_foto_original_name') @@ -342,6 +388,18 @@ public function table(Table $table): Table return filled($record->lettura_foto_path) ? '1 foto' : '—'; }) + ->tooltip(function (StabileServizioLettura $record): string { + $photos = $this->getReadingPhotoEntries($record); + if ($photos === []) { + return 'Nessuna foto'; + } + + return implode("\n", array_map( + fn(array $photo): string => (string) ($photo['name'] ?? basename((string) ($photo['path'] ?? 'foto'))), + $photos + )); + }) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('protocollo_numero') @@ -356,55 +414,72 @@ public function table(Table $table): Table return trim($categoria . ($numero !== '' ? (' #' . $numero) : '')); }) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('prossima_lettura_scadenza_at') ->label('Scadenza prossima lettura') ->dateTime('d/m/Y H:i') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('deadline_lettura_at') ->label('Deadline campagna') ->dateTime('d/m/Y H:i') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('sollecito_inviato_at') ->label('Sollecito') ->dateTime('d/m/Y H:i') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('rilevatore_tipo') ->label('Rilevatore') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('lettura_precedente_valore') ->label('Lettura precedente') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('archivio_documentale_path') ->label('Archivio documentale') - ->wrap() + ->limit(36) + ->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('fatturaElettronica.numero_fattura') ->label('Fattura FE') - ->url(fn(StabileServizioLettura $record): ?string => $record->fattura_elettronica_id - ? FatturaElettronicaScheda::getUrl(['id' => (int) $record->fattura_elettronica_id], panel : 'admin-filament') - : null) - ->openUrlInNewTab() + ->formatStateUsing(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), - TextColumn::make('voceSpesa.descrizione')->label('Voce spesa')->wrap()->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('voceSpesa.descrizione') + ->label('Voce spesa') + ->limit(30) + ->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) + ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('voceSpesa.tipo_gestione') ->label('Gestione') ->formatStateUsing(fn($state) => $gestioneOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—')) + ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('fornitore.ragione_sociale')->label('Fornitore')->wrap()->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('fornitore.ragione_sociale') + ->label('Fornitore') + ->limit(30) + ->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—') + ->extraAttributes(['class' => 'text-[11px] leading-tight']) + ->toggleable(isToggledHiddenByDefault: true), - TextColumn::make('created_at')->label('Creato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true), + TextColumn::make('created_at')->label('Creato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']), ]) ->headerActions([ Action::make('toggleScope') @@ -574,20 +649,47 @@ public function table(Table $table): Table Action::make('apriFoto') ->label('Foto') ->icon('heroicon-o-photo') - ->url(fn(StabileServizioLettura $record): ?string => $this->resolveReadingPrimaryPhotoUrl($record)) - ->openUrlInNewTab() - ->visible(fn(StabileServizioLettura $record): bool => $this->resolveReadingPrimaryPhotoUrl($record) !== null), + ->button() + ->color('gray') + ->modalHeading('Foto contatore') + ->modalWidth('5xl') + ->modalSubmitAction(false) + ->modalCancelActionLabel('Chiudi') + ->modalContent(function (StabileServizioLettura $record): View { + return view('filament.pages.condomini.partials.lettura-servizio-foto-modal', [ + 'record' => $record, + 'photos' => $this->getReadingPhotoEntries($record), + 'mapUrl' => $this->resolveReadingMapUrl($record), + 'assigned' => $this->resolveReadingAssignmentLabel($record), + ]); + }) + ->visible(fn(StabileServizioLettura $record): bool => $this->getReadingPhotoEntries($record) !== []), Action::make('mappaFoto') ->label('Mappa') ->icon('heroicon-o-map') + ->button() + ->color('gray') ->url(fn(StabileServizioLettura $record): ?string => $this->resolveReadingMapUrl($record)) ->openUrlInNewTab() ->visible(fn(StabileServizioLettura $record): bool => $this->resolveReadingMapUrl($record) !== null), + Action::make('apriFatturaFe') + ->label('FE') + ->icon('heroicon-o-receipt-percent') + ->button() + ->color('primary') + ->url(fn(StabileServizioLettura $record): ?string => $record->fattura_elettronica_id + ? FatturaElettronicaScheda::getUrl(['record' => (int) $record->fattura_elettronica_id], panel: 'admin-filament') + : null) + ->openUrlInNewTab() + ->visible(fn(StabileServizioLettura $record): bool => (int) ($record->fattura_elettronica_id ?? 0) > 0), + Action::make('riattribuisci') ->label('Riattribuisci') ->icon('heroicon-o-user-circle') + ->button() + ->color('gray') ->form([ Select::make('unita_immobiliare_id') ->label('Unità / nominativo') @@ -646,6 +748,7 @@ public function table(Table $table): Table Action::make('spostaContatoreComune') ->label('Sposta a contatore comune') ->icon('heroicon-o-arrow-path-rounded-square') + ->button() ->color('warning') ->form([ Select::make('stabile_servizio_id') @@ -728,6 +831,8 @@ public function table(Table $table): Table Action::make('edit') ->label('Modifica') ->icon('heroicon-o-pencil-square') + ->button() + ->color('gray') ->form([ Select::make('stabile_servizio_id') ->label('Servizio') @@ -847,6 +952,7 @@ public function table(Table $table): Table Action::make('delete') ->label('Elimina') ->icon('heroicon-o-trash') + ->button() ->color('danger') ->requiresConfirmation() ->action(fn(StabileServizioLettura $record) => $record->delete()), @@ -1345,21 +1451,55 @@ private function resolveReadingAssignmentLabel(StabileServizioLettura $record): return $parts !== [] ? implode(' - ', $parts) : 'Contatore generale / bene comune'; } - private function resolveReadingPrimaryPhotoPath(StabileServizioLettura $record): ?string + /** + * @return array + */ + private function getReadingPhotoEntries(StabileServizioLettura $record): array { + $entries = []; + + $append = function (string $path, ?string $name = null, $lat = null, $lng = null) use (&$entries): void { + $path = trim($path); + if ($path === '' || isset($entries[$path]) || ! Storage::disk('public')->exists($path)) { + return; + } + + $entries[$path] = [ + 'name' => trim((string) ($name ?? '')) !== '' ? trim((string) $name) : basename($path), + 'path' => $path, + 'url' => Storage::disk('public')->url($path), + 'latitude' => is_numeric($lat) ? (float) $lat : null, + 'longitude' => is_numeric($lng) ? (float) $lng : null, + ]; + }; + if (filled($record->lettura_foto_path)) { - return (string) $record->lettura_foto_path; + $append( + (string) $record->lettura_foto_path, + (string) ($record->lettura_foto_original_name ?? ''), + data_get($record->lettura_foto_metadata, 'gps_decimal.lat'), + data_get($record->lettura_foto_metadata, 'gps_decimal.lng') + ); } $photos = is_array(data_get($record->lettura_foto_metadata, 'photos')) ? data_get($record->lettura_foto_metadata, 'photos') : []; foreach ($photos as $photo) { - $path = trim((string) ($photo['path'] ?? '')); - if ($path !== '') { - return $path; - } + $append( + (string) ($photo['path'] ?? ''), + (string) ($photo['original_name'] ?? $photo['stored_basename'] ?? ''), + data_get($photo, 'metadata.gps_decimal.lat'), + data_get($photo, 'metadata.gps_decimal.lng') + ); } - return null; + return array_values($entries); + } + + private function resolveReadingPrimaryPhotoPath(StabileServizioLettura $record): ?string + { + $photos = $this->getReadingPhotoEntries($record); + + return $photos !== [] ? (string) ($photos[0]['path'] ?? '') : null; } private function resolveReadingPrimaryPhotoUrl(StabileServizioLettura $record): ?string diff --git a/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php b/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php index ea4c1d2..10ebb4d 100644 --- a/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php +++ b/app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php @@ -2,10 +2,15 @@ namespace App\Filament\Pages\Contabilita; use App\Filament\Pages\Contabilita\FatturaElettronicaScheda; +use App\Models\Documento; use App\Jobs\RunCassettoFiscaleDownload; use App\Models\FatturaElettronica; use App\Models\Stabile; use App\Models\User; +use App\Services\Consumi\AcquaPdfTextParser; +use App\Services\Consumi\ConsumiAcquaIngestionService; +use App\Services\Consumi\ConsumiAcquaTariffeIngestionService; +use App\Services\Documenti\PdfTextExtractionService; use App\Services\FattureElettroniche\CassettoFiscaleDownloadService; use App\Services\FattureElettroniche\FatturaElettronicaImporter; use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService; @@ -110,6 +115,169 @@ private function safeUtf8(string $value): string return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value) ?? $value; } + /** + * @return array{status: string, created_letture?: int, updated_letture?: int, tickets?: int, message?: string} + */ + private function autoLinkWaterReadingsForInvoice(int $fatturaId, int $userId): array + { + $fattura = FatturaElettronica::query()->find($fatturaId); + if (! $fattura instanceof FatturaElettronica) { + return ['status' => 'skipped', 'message' => 'Fattura non trovata']; + } + + $hasPdf = is_string($fattura->allegato_pdf_path ?? null) && trim((string) $fattura->allegato_pdf_path) !== ''; + $hasStoredPayload = is_string($fattura->consumo_raw ?? null) && trim((string) $fattura->consumo_raw) !== ''; + if (! $hasPdf && ! $hasStoredPayload) { + return ['status' => 'skipped', 'message' => 'Nessun PDF/payload acqua disponibile']; + } + + try { + if ($userId > 0) { + app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($fattura, $userId); + $fattura->refresh(); + } + } catch (\Throwable) { + // ignore + } + + $documento = Documento::query() + ->where('documentable_type', FatturaElettronica::class) + ->where('documentable_id', (int) $fattura->id) + ->first(); + + $parsed = []; + if ($documento instanceof Documento) { + $text = is_string($documento->contenuto_ocr) ? trim((string) $documento->contenuto_ocr) : ''; + if ($text === '') { + try { + $result = app(PdfTextExtractionService::class)->extractAndStore($documento, false); + if (($result['status'] ?? null) === 'ok') { + $documento->refresh(); + $text = is_string($documento->contenuto_ocr) ? trim((string) $documento->contenuto_ocr) : ''; + } + } catch (\Throwable) { + $text = ''; + } + } + + if ($text !== '') { + $parsed = app(AcquaPdfTextParser::class)->parse($text); + } + } + + $storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : ''; + if ($storedRaw !== '' && $parsed === []) { + $decoded = json_decode($storedRaw, true); + if (is_array($decoded)) { + $parsed = $decoded; + } + } + + $codici = is_array($parsed['codici'] ?? null) ? $parsed['codici'] : []; + $contatore = is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : []; + $consumi = is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []; + $generale = is_array($parsed['generale'] ?? null) ? $parsed['generale'] : []; + $finestra = is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : []; + $letture = is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : []; + $quadro = is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : []; + $tariffe = is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : []; + $ivaInfo = is_array($parsed['iva'] ?? null) ? $parsed['iva'] : []; + + $hasAny = false; + foreach ([$codici['utenza'] ?? null, $codici['cliente'] ?? null, $codici['contratto'] ?? null, $contatore['matricola'] ?? null] as $value) { + if (is_string($value) && trim($value) !== '') { + $hasAny = true; + break; + } + } + + $hasTariffData = count(array_filter([ + $generale['periodicita_fatturazione'] ?? null, + $tariffe['profilo'] ?? null, + $ivaInfo['codice'] ?? null, + $quadro['quota_fissa'] ?? null, + ], static fn($value): bool => $value !== null && $value !== '')) > 0; + + if (! $hasAny && count($consumi) < 1 && ! $hasTariffData) { + return ['status' => 'skipped', 'message' => 'Nessun dato acqua utile rilevato']; + } + + $payload = [ + 'type' => 'acqua', + 'source' => 'pdf_ocr', + 'codici' => [ + 'utenza' => $codici['utenza'] ?? null, + 'cliente' => $codici['cliente'] ?? null, + 'contratto' => $codici['contratto'] ?? null, + ], + 'contatore' => [ + 'matricola' => $contatore['matricola'] ?? null, + ], + 'consumi' => array_values($consumi), + 'generale' => $generale, + 'finestra_autolettura' => $finestra, + 'riepilogo_letture' => array_values($letture), + 'quadro_dettaglio' => $quadro, + 'tariffe' => $tariffe, + 'iva' => [ + 'codice' => $ivaInfo['codice'] ?? null, + 'aliquota_percentuale' => $ivaInfo['aliquota_percentuale'] ?? null, + 'descrizione' => $ivaInfo['descrizione'] ?? null, + ], + 'parsed_at' => now()->toISOString(), + ]; + + $totalMc = null; + $reference = null; + if ($consumi !== []) { + $sum = 0.0; + $hasNumeric = false; + foreach ($consumi as $consumo) { + if (isset($consumo['valore']) && is_numeric($consumo['valore'])) { + $sum += (float) $consumo['valore']; + $hasNumeric = true; + } + } + if ($hasNumeric) { + $totalMc = $sum; + } + + $first = $consumi[0] ?? []; + $dal = $first['dal'] ?? null; + $al = $first['al'] ?? null; + if (is_string($dal) && is_string($al) && $dal !== '' && $al !== '') { + $reference = $dal . ' - ' . $al; + } + } + + $fattura->consumo_unita = 'mc'; + $fattura->consumo_valore = $totalMc; + $fattura->consumo_riferimento = $reference; + $fattura->consumo_raw = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + $fattura->save(); + + try { + app(ConsumiAcquaTariffeIngestionService::class)->ingest($fattura, $parsed, null); + } catch (\Throwable) { + // best-effort + } + + $ingestion = app(ConsumiAcquaIngestionService::class)->ingest($fattura, $parsed, null, $userId, false, null); + if (($ingestion['status'] ?? null) === 'ok') { + try { + app(ConsumiAcquaTariffeIngestionService::class)->ingest( + $fattura, + $parsed, + isset($ingestion['servizio_id']) && is_numeric($ingestion['servizio_id']) ? (int) $ingestion['servizio_id'] : null + ); + } catch (\Throwable) { + // best-effort + } + } + + return $ingestion; + } + public int $cassettoAnno; /** @@ -1006,6 +1174,8 @@ protected function getHeaderActions(): array $imported = 0; $duplicates = 0; $errors = 0; + $waterCreated = 0; + $waterUpdated = 0; $duplicateDetails = []; $errorDetails = []; @@ -1125,6 +1295,14 @@ protected function getHeaderActions(): array // ignore } + if (in_array((string) ($result['status'] ?? ''), ['imported', 'duplicate'], true) && is_numeric($result['id'] ?? null)) { + $waterResult = $this->autoLinkWaterReadingsForInvoice((int) $result['id'], (int) $user->id); + if (($waterResult['status'] ?? null) === 'ok') { + $waterCreated += (int) ($waterResult['created_letture'] ?? 0); + $waterUpdated += (int) ($waterResult['updated_letture'] ?? 0); + } + } + if (($result['status'] ?? null) === 'imported') { $imported++; } elseif (($result['status'] ?? null) === 'duplicate') { @@ -1228,6 +1406,14 @@ protected function getHeaderActions(): array // ignore } + if (in_array((string) ($result['status'] ?? ''), ['imported', 'duplicate'], true) && is_numeric($result['id'] ?? null)) { + $waterResult = $this->autoLinkWaterReadingsForInvoice((int) $result['id'], (int) $user->id); + if (($waterResult['status'] ?? null) === 'ok') { + $waterCreated += (int) ($waterResult['created_letture'] ?? 0); + $waterUpdated += (int) ($waterResult['updated_letture'] ?? 0); + } + } + if (($result['status'] ?? null) === 'imported') { $imported++; } elseif (($result['status'] ?? null) === 'duplicate') { @@ -1314,7 +1500,7 @@ protected function getHeaderActions(): array Notification::make() ->title('Import completato') - ->body($this->safeUtf8("Importate: {$imported} · Duplicate: {$duplicates} · Errori: {$errors}" . $logNote)) + ->body($this->safeUtf8("Importate: {$imported} · Duplicate: {$duplicates} · Errori: {$errors} · Letture acqua create: {$waterCreated} · aggiornate: {$waterUpdated}" . $logNote)) ->success() ->send(); diff --git a/docs/ai/SESSION_HANDOFF.md b/docs/ai/SESSION_HANDOFF.md index 746c4a5..5d57aa5 100644 --- a/docs/ai/SESSION_HANDOFF.md +++ b/docs/ai/SESSION_HANDOFF.md @@ -10,6 +10,8 @@ ## Stato operativo reale alla chiusura sessione - 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` + - compattare la UI di `letture-servizi` con pulsanti, tooltip, miniatura e modal foto + - agganciare automaticamente il parsing acqua alle FE ricevute importate quando esiste PDF/payload utilizzabile ## Verifiche fatte e confermate @@ -21,9 +23,28 @@ ## Verifiche fatte e confermate - 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. +- La pagina `fatture-ricevute` continua a filtrare per `stabile_id` attivo; nel DB locale per `stabile_id=25` esistono FE 2025 ricevute. +- Per `stabile_id=25` esistono FE 2025 con `allegato_pdf_path` e in diversi casi `consumo_raw`, quindi il nuovo aggancio acqua post-import ha input reali da elaborare. +- `php artisan view:clear && php artisan view:cache` continua a passare dopo i ritocchi UI e il nuovo post-processing FE. +- `php artisan route:list` continua a caricare `755` route. ## File toccati in questa fase +- `app/Filament/Pages/Condomini/LettureServiziArchivio.php` + - colonna miniatura foto + - testi piu compatti (`text-[11px]`) + - tooltip al posto di testo lungo per servizio/attribuzione/check/voce/fornitore + - azioni riga rese pulsanti + - modal foto invece dell'apertura diretta del file + - pulsante FE dedicato in riga +- `resources/views/filament/pages/condomini/partials/lettura-servizio-foto-modal.blade.php` + - nuova vista modal per anteprima foto contatore +- `app/Filament/Pages/Contabilita/FattureElettronicheP7mRicevute.php` + - nuovo post-processing `autoLinkWaterReadingsForInvoice()` dopo import FE + - sincronizzazione `consumo_raw`/consumi FE + - chiamata a `ConsumiAcquaIngestionService` e `ConsumiAcquaTariffeIngestionService` in best-effort + - notifica finale estesa con conteggio letture acqua create/aggiornate + - `app/Providers/Filament/AdminFilamentPanelProvider.php` - chain Filament riparata - footer hook rimosso dal panel diff --git a/resources/views/filament/pages/condomini/partials/lettura-servizio-foto-modal.blade.php b/resources/views/filament/pages/condomini/partials/lettura-servizio-foto-modal.blade.php new file mode 100644 index 0000000..5580bfc --- /dev/null +++ b/resources/views/filament/pages/condomini/partials/lettura-servizio-foto-modal.blade.php @@ -0,0 +1,46 @@ +@php + $photos = is_array($photos ?? null) ? $photos : []; + $primary = $photos[0] ?? null; +@endphp + +
+
+
+
{{ $assigned ?? 'Contatore' }}
+
+ Riga #{{ (int) ($record->id ?? 0) }} + @if(! empty($record->periodo_dal) || ! empty($record->periodo_al)) + · Periodo {{ optional($record->periodo_dal)->format('d/m/Y') ?: '—' }} → {{ optional($record->periodo_al)->format('d/m/Y') ?: '—' }} + @endif +
+
+ @if(is_string($mapUrl ?? null) && $mapUrl !== '') + + Apri mappa + + @endif +
+ + @if($primary) +
+ {{ $primary['name'] }} +
+ @endif + + @if(count($photos) > 1) +
+ @foreach($photos as $photo) + + {{ $photo['name'] }} +
{{ $photo['name'] }}
+
+ @endforeach +
+ @endif + + @if($photos === []) +
+ Nessuna foto disponibile per questa lettura. +
+ @endif +
\ No newline at end of file