netgescon-day0/resources/views/filament/pages/supporto/ticket-acqua-generale.blade.php

375 lines
26 KiB
PHP

<x-filament-panels::page>
<div class="space-y-4">
@if ($errors->any())
<div class="rounded-xl border border-rose-300 bg-rose-50 p-3 text-sm text-rose-800">
<div class="font-semibold">Controlla i dati prima di salvare</div>
<ul class="mt-2 list-disc pl-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 class="text-lg font-semibold text-slate-900">Lettura contatore generale acqua</h2>
<div class="mt-1 text-sm text-slate-600">Pagina operativa per autoletture Acea del contatore generale, con foto geolocalizzate, storico per contratto e scadenze da calendario per amministrazione o collaboratori.</div>
</div>
<div class="rounded-full bg-sky-50 px-3 py-1 text-xs font-semibold text-sky-800">Bozza {{ $generalWaterDraftProtocol }}</div>
</div>
</div>
<div class="grid gap-4 xl:grid-cols-[1.05fr_0.95fr]">
<div class="space-y-4">
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="grid gap-4 md:grid-cols-2">
<label class="block text-sm">
<span class="mb-1 block font-medium">Stabile</span>
<select wire:model.live="stabileId" class="w-full rounded-lg border-gray-300">
<option value="">Seleziona stabile</option>
@foreach($stabileOptions as $option)
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
</label>
<label class="block text-sm">
<span class="mb-1 block font-medium">Contratto / servizio acqua</span>
<select wire:model.live="servizioId" class="w-full rounded-lg border-gray-300">
<option value="">Seleziona contratto</option>
@foreach($servizioOptions as $option)
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
<div class="mt-1 text-[11px] text-slate-500">Se lo stabile ha piu contratti, la lettura generale va agganciata a quello corretto.</div>
</label>
<label class="block text-sm">
<span class="mb-1 block font-medium">Letturista</span>
<select wire:model.live="rilevatoreUserId" class="w-full rounded-lg border-gray-300">
<option value="">Seleziona letturista</option>
@foreach($readerOptions as $option)
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
@endforeach
</select>
</label>
<label class="block text-sm">
<span class="mb-1 block font-medium">Nominativo che invia la lettura</span>
<input type="text" wire:model.defer="rilevatoreNome" class="w-full rounded-lg border-gray-300" placeholder="Nome e cognome" />
</label>
</div>
</div>
@php($service = $this->selectedServiceSnapshot)
@if($service)
<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="mt-3 grid gap-3 md:grid-cols-2 text-sm text-emerald-900">
<div><span class="font-medium">Servizio:</span> {{ $service['nome'] ?: 'n/d' }}</div>
<div><span class="font-medium">Fornitore:</span> {{ $service['fornitore'] ?: 'n/d' }}</div>
<div><span class="font-medium">Codice utenza:</span> {{ $service['codice_utenza'] ?: 'n/d' }}</div>
<div><span class="font-medium">Codice cliente:</span> {{ $service['codice_cliente'] ?: 'n/d' }}</div>
<div><span class="font-medium">Codice contratto:</span> {{ $service['codice_contratto'] ?: 'n/d' }}</div>
<div><span class="font-medium">Matricola contatore:</span> {{ $service['matricola'] ?: 'n/d' }}</div>
</div>
<div class="mt-3 rounded-xl border border-emerald-300 bg-white p-3 text-xs text-slate-700">
<div class="font-semibold text-slate-900">Formato rapido Acea</div>
<div class="mt-1">SMS a 3399941808: <span class="font-mono">{{ $service['acea_sms_preview'] !== '' ? $service['acea_sms_preview'] : 'codiceutenza#codicecliente#autolettura' }}</span></div>
<div class="mt-1">Numero verde: <span class="font-medium">800 130 331</span></div>
<div class="mt-1">Area clienti: <span class="font-medium">www.aceaato2.it</span></div>
<div class="mt-1">Cartella archivio anno: <span class="font-medium">{{ $service['year_folder'] }}</span></div>
</div>
</div>
@endif
<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 shadow-sm">
<div class="grid gap-4 md:grid-cols-2">
<label class="block text-sm">
<span class="mb-1 block font-medium text-slate-900">Lettura effettiva</span>
<input type="number" step="0.001" min="0" wire:model.defer="letturaAttuale" class="w-full rounded-lg border-sky-300 bg-white text-2xl font-semibold text-slate-900" placeholder="Es. 1284.350" />
</label>
<label class="block text-sm">
<span class="mb-1 block font-medium text-slate-900">Data lettura</span>
<input type="date" wire:model.defer="dataLettura" class="w-full rounded-lg border-sky-300 bg-white text-slate-900" />
</label>
</div>
<div class="mt-2 text-xs text-slate-600">La lettura viene salvata nello storico del contratto selezionato, separata dalle singole unita ma con lo stesso standard foto e metadati.</div>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-sm font-semibold text-slate-900">Finestra Acea e calendario operativo</div>
<div class="mt-3 grid gap-4 md:grid-cols-2">
<label class="block text-sm">
<span class="mb-1 block font-medium">Inizio finestra autolettura</span>
<input type="date" wire:model.defer="aceaWindowStart" class="w-full rounded-lg border-gray-300" />
</label>
<label class="block text-sm">
<span class="mb-1 block font-medium">Fine finestra autolettura</span>
<input type="date" wire:model.defer="aceaWindowEnd" class="w-full rounded-lg border-gray-300" />
</label>
<label class="block text-sm">
<span class="mb-1 block font-medium">Data passaggio incaricato Acea</span>
<input type="date" wire:model.defer="aceaVisitDate" class="w-full rounded-lg border-gray-300" />
</label>
<label class="block text-sm">
<span class="mb-1 block font-medium">Canale previsto invio lettura</span>
<select wire:model.defer="aceaAutoSendChannel" class="w-full rounded-lg border-gray-300">
<option value="manuale">Manuale studio</option>
<option value="sms">SMS Acea</option>
<option value="myacea">MyAcea Acqua</option>
<option value="telefono">Numero verde</option>
<option value="api_futura">Invio automatico futuro</option>
</select>
</label>
<div class="grid gap-4 md:grid-cols-2">
<label class="block text-sm">
<span class="mb-1 block font-medium">Fascia da</span>
<input type="time" wire:model.defer="aceaVisitTimeFrom" class="w-full rounded-lg border-gray-300" />
</label>
<label class="block text-sm">
<span class="mb-1 block font-medium">Fascia a</span>
<input type="time" wire:model.defer="aceaVisitTimeTo" class="w-full rounded-lg border-gray-300" />
</label>
</div>
</div>
<div class="mt-2 text-xs text-slate-500">Quando salvi la lettura, creo anche le scadenze operative per la finestra Acea e per il passaggio dell'incaricato se hai compilato le date.</div>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<label class="block text-sm">
<span class="mb-1 block font-medium">Note operative</span>
<textarea rows="4" wire:model.defer="noteGenerali" class="w-full rounded-lg border-gray-300" placeholder="Es. accesso locale contatori, due contratti nello stabile, lettura inviata via MyAcea oppure da confermare via SMS..."></textarea>
</label>
</div>
</div>
<div class="space-y-4">
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-sm font-semibold text-slate-900">Ultima lettura generale nota</div>
@if($lastGeneralReading)
<div class="mt-3 space-y-1 text-sm text-slate-700">
<div><span class="font-medium">Valore:</span> {{ number_format((float) $lastGeneralReading['value'], 3, ',', '.') }}</div>
<div><span class="font-medium">Data:</span> {{ $lastGeneralReading['date'] ?: 'n/d' }}</div>
<div><span class="font-medium">Letturista:</span> {{ $lastGeneralReading['reader'] ?: 'n/d' }}</div>
<div><span class="font-medium">Protocollo:</span> {{ $lastGeneralReading['protocol'] ?: 'n/d' }}</div>
</div>
@else
<div class="mt-3 text-sm text-slate-500">Nessuna lettura generale ancora registrata per questo contratto.</div>
@endif
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<label class="block text-sm"
x-data="{
localPreviews: [],
localClientMetadata: [],
init() {
window.__ticketAcquaGeneralePreviewState = window.__ticketAcquaGeneralePreviewState || [];
window.__ticketAcquaGeneraleClientMetadataState = window.__ticketAcquaGeneraleClientMetadataState || [];
this.localPreviews = [...window.__ticketAcquaGeneralePreviewState];
this.localClientMetadata = [...window.__ticketAcquaGeneraleClientMetadataState];
this.syncClientMetadataToWire();
},
persist() {
window.__ticketAcquaGeneralePreviewState = [...this.localPreviews];
},
persistClientMetadata() {
window.__ticketAcquaGeneraleClientMetadataState = [...this.localClientMetadata];
this.syncClientMetadataToWire();
},
revoke(items) {
items.forEach((item) => {
if (item.url) {
URL.revokeObjectURL(item.url);
}
});
},
syncClientMetadataToWire() {
$wire.set('generalWaterClientMetadata', this.localClientMetadata.map((item) => ({
source: item.source,
name: item.name,
size: item.size,
captured_at: item.captured_at || null,
device: item.device || null,
gps_decimal: item.gps_decimal || null,
gps_accuracy_meters: item.gps_accuracy_meters || null,
})), false);
},
getDeviceLabel() {
if (navigator.userAgentData?.platform) {
return navigator.userAgentData.platform;
}
return navigator.platform || navigator.userAgent || '';
},
async readGeolocation() {
if (!navigator.geolocation) {
return null;
}
return await new Promise((resolve) => {
navigator.geolocation.getCurrentPosition(
(position) => resolve({
gps_decimal: {
lat: position.coords.latitude,
lng: position.coords.longitude,
},
gps_accuracy_meters: position.coords.accuracy || null,
}),
() => resolve(null),
{
enableHighAccuracy: true,
maximumAge: 30000,
timeout: 8000,
},
);
});
},
async buildClientMetadata(file) {
const base = {
source: 'camera',
name: file?.name || '',
size: file?.size || 0,
captured_at: new Date(file?.lastModified || Date.now()).toISOString(),
device: this.getDeviceLabel(),
};
const geo = await this.readGeolocation();
return geo ? { ...base, ...geo } : base;
},
mergeClientMetadata(items) {
const existing = new Map(this.localClientMetadata.map((item) => [`${item.name}|${item.size}`, item]));
items.forEach((item) => {
if (!item?.name) {
return;
}
existing.set(`${item.name}|${item.size}`, item);
});
this.localClientMetadata = Array.from(existing.values());
this.persistClientMetadata();
},
async addFiles(event) {
const batchId = Date.now();
const files = Array.from(event?.target?.files || []);
const mapped = files.map((file, index) => ({
key: `${batchId}-${index}-${file.name}-${file.size}`,
name: file.name,
size: file.size || 0,
url: (file.type || '').startsWith('image/') ? URL.createObjectURL(file) : null,
}));
const existing = new Map(this.localPreviews.map((item) => [item.key, item]));
mapped.forEach((item) => existing.set(item.key, item));
this.localPreviews = Array.from(existing.values());
this.mergeClientMetadata(await Promise.all(files.map((file) => this.buildClientMetadata(file))));
this.persist();
if (event?.target) {
event.target.value = null;
}
},
previewFor(name, size = 0) {
const exact = this.localPreviews.find((item) => item.name === name && Number(item.size || 0) === Number(size || 0));
return exact?.url || null;
},
clearAll() {
this.revoke(this.localPreviews);
this.localPreviews = [];
this.localClientMetadata = [];
this.persist();
this.persistClientMetadata();
},
}"
x-on:ticket-acqua-generale-draft-reset.window="clearAll()"
>
<span class="mb-2 block font-medium">Foto contatore generale</span>
<label class="inline-flex cursor-pointer items-center rounded-md bg-emerald-600 px-3 py-2 text-xs font-medium text-white hover:bg-emerald-500">
Scatta o aggiungi foto
<input type="file" wire:model="pendingGeneralWaterPhotos" multiple accept="image/*" capture="environment" class="hidden" x-on:change="addFiles($event)" />
</label>
<div class="mt-1 text-xs text-gray-500">Puoi accodare fino a 4 immagini del contatore generale e della targhetta matricola.</div>
<div wire:loading wire:target="pendingGeneralWaterPhotos" class="mt-2 text-xs font-medium text-amber-700">Caricamento foto in corso.</div>
@if(count($this->selectedGeneralWaterUploads) > 0)
<div class="mt-4 grid gap-3 sm:grid-cols-2">
@foreach($this->selectedGeneralWaterUploads as $upload)
<div class="rounded-xl border bg-slate-50 p-3 text-xs shadow-sm">
<div class="mb-2 aspect-[4/3] overflow-hidden rounded-lg border bg-white" x-show="Boolean(@js($upload['preview_url']) || previewFor(@js($upload['original_name']), @js($upload['size'] ?? 0)))">
<img x-bind:src="@js($upload['preview_url']) || previewFor(@js($upload['original_name']), @js($upload['size'] ?? 0))" alt="{{ $upload['name'] }}" class="h-full w-full object-cover" />
</div>
<div class="rounded-lg border bg-white px-3 py-2 text-[11px] text-slate-700">
<div class="font-medium">{{ $upload['name'] }}</div>
<div class="mt-1 text-emerald-700">Protocollo bozza: {{ $upload['protocol'] }}</div>
<div class="mt-1 text-slate-500">Origine: {{ $upload['original_name'] }}</div>
</div>
@php
$metadata = is_array($upload['metadata'] ?? null) ? $upload['metadata'] : [];
$gpsLat = data_get($metadata, 'gps_decimal.lat');
$gpsLng = data_get($metadata, 'gps_decimal.lng');
$hasGps = is_numeric($gpsLat) && is_numeric($gpsLng);
$device = trim((string) data_get($metadata, 'device', ''));
@endphp
@if($hasGps || $device !== '')
<div class="mt-2 rounded-lg border border-slate-200 bg-white p-2 text-[11px] text-slate-700">
@if($hasGps)
<div><span class="font-medium">Coordinate:</span> {{ number_format((float) $gpsLat, 5, '.', '') }}, {{ number_format((float) $gpsLng, 5, '.', '') }}</div>
@endif
@if($device !== '')
<div><span class="font-medium">Dispositivo:</span> {{ $device }}</div>
@endif
</div>
@endif
<label class="mt-2 block">
<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" />
</label>
<button type="button" wire:click="removeGeneralWaterPhoto({{ (int) $upload['index'] }})" class="mt-2 inline-flex items-center rounded-md bg-rose-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-rose-500">Rimuovi</button>
</div>
@endforeach
</div>
@else
<div class="mt-3 rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-6 text-center text-sm text-slate-500">Nessuna foto in coda. Aggiungi almeno la foto display e, se possibile, matricola/contratto esposto vicino al contatore.</div>
@endif
</label>
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<div class="text-sm font-semibold text-slate-900">Storico rapido contratto</div>
@if(count($this->recentGeneralReadings) > 0)
<div class="mt-3 space-y-2">
@foreach($this->recentGeneralReadings as $row)
<div class="rounded-xl border bg-slate-50 px-3 py-2 text-xs text-slate-700">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="font-medium">{{ $row['date'] ?: 'n/d' }} · {{ $row['protocollo'] ?: 'senza protocollo' }}</div>
<div>{{ $row['workflow'] ?: 'n/d' }}</div>
</div>
<div class="mt-1">Valore: <span class="font-medium">{{ $row['value'] !== null ? number_format((float) $row['value'], 3, ',', '.') . ' mc' : 'n/d' }}</span></div>
@if($row['consumo'] !== null)
<div class="mt-1">Consumo: {{ number_format((float) $row['consumo'], 3, ',', '.') }} mc</div>
@endif
@if($row['reader'] !== '')
<div class="mt-1">Letturista: {{ $row['reader'] }}</div>
@endif
</div>
@endforeach
</div>
@else
<div class="mt-3 text-sm text-slate-500">Storico ancora vuoto per questo contratto.</div>
@endif
</div>
<div class="rounded-2xl border bg-white p-4 shadow-sm">
<x-filament::button wire:click="registraLetturaGenerale" wire:loading.attr="disabled" wire:target="registraLetturaGenerale,pendingGeneralWaterPhotos">
<span wire:loading.remove wire:target="registraLetturaGenerale">Salva lettura generale</span>
<span wire:loading wire:target="registraLetturaGenerale">Salvataggio...</span>
</x-filament::button>
</div>
</div>
</div>
</div>
</x-filament-panels::page>