Refine letture UI and auto-link FE water readings
This commit is contained in:
parent
2639c0a0e3
commit
fe9e8adb72
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user