Refine letture UI and auto-link FE water readings

This commit is contained in:
michele 2026-04-14 10:14:42 +00:00
parent 2639c0a0e3
commit fe9e8adb72
4 changed files with 423 additions and 30 deletions

View File

@ -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<int, array{name: string, path: string, url: string, latitude: float|null, longitude: float|null}>
*/
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

View File

@ -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();

View File

@ -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

View File

@ -0,0 +1,46 @@
@php
$photos = is_array($photos ?? null) ? $photos : [];
$primary = $photos[0] ?? null;
@endphp
<div class="space-y-4">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<div class="text-sm font-semibold text-slate-900">{{ $assigned ?? 'Contatore' }}</div>
<div class="text-xs text-slate-500">
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
</div>
</div>
@if(is_string($mapUrl ?? null) && $mapUrl !== '')
<a href="{{ $mapUrl }}" target="_blank" rel="noreferrer" class="inline-flex items-center rounded-md bg-slate-800 px-3 py-2 text-xs font-medium text-white hover:bg-slate-700">
Apri mappa
</a>
@endif
</div>
@if($primary)
<div class="overflow-hidden rounded-xl border border-slate-200 bg-slate-50">
<img src="{{ $primary['url'] }}" alt="{{ $primary['name'] }}" class="max-h-[60vh] w-full object-contain bg-white" />
</div>
@endif
@if(count($photos) > 1)
<div class="grid grid-cols-2 gap-3 md:grid-cols-4">
@foreach($photos as $photo)
<a href="{{ $photo['url'] }}" target="_blank" rel="noreferrer" class="overflow-hidden rounded-lg border border-slate-200 bg-white transition hover:border-slate-400">
<img src="{{ $photo['url'] }}" alt="{{ $photo['name'] }}" class="h-28 w-full object-cover" />
<div class="border-t border-slate-100 px-2 py-2 text-[11px] text-slate-600">{{ $photo['name'] }}</div>
</a>
@endforeach
</div>
@endif
@if($photos === [])
<div class="rounded-lg border border-dashed border-slate-300 bg-slate-50 px-4 py-6 text-sm text-slate-500">
Nessuna foto disponibile per questa lettura.
</div>
@endif
</div>