Add metadata source badges and ticket acqua light
This commit is contained in:
parent
755e3a5015
commit
554212fd0b
22
app/Filament/Pages/Supporto/TicketAcquaLight.php
Normal file
22
app/Filament/Pages/Supporto/TicketAcquaLight.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
namespace App\Filament\Pages\Supporto;
|
||||
|
||||
use BackedEnum;
|
||||
use UnitEnum;
|
||||
|
||||
class TicketAcquaLight extends TicketAcqua
|
||||
{
|
||||
protected static ?string $navigationLabel = 'Ticket Acqua Light';
|
||||
|
||||
protected static ?string $title = 'Ticket Acqua Light';
|
||||
|
||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-bolt';
|
||||
|
||||
protected static UnitEnum|string|null $navigationGroup = 'Mobile';
|
||||
|
||||
protected static ?int $navigationSort = 27;
|
||||
|
||||
protected static ?string $slug = 'supporto/ticket-acqua-light';
|
||||
|
||||
protected string $view = 'filament.pages.supporto.ticket-acqua-light';
|
||||
}
|
||||
|
|
@ -422,6 +422,10 @@ public function getAttachmentDetails(TicketAttachment $attachment): array
|
|||
$details['file_label'] = trim((string) $metadata['stored_name']);
|
||||
}
|
||||
|
||||
if ($details['capture_source'] === null && filled($metadata['capture_source'] ?? null)) {
|
||||
$details['capture_source'] = trim((string) $metadata['capture_source']);
|
||||
}
|
||||
|
||||
return $details;
|
||||
}
|
||||
|
||||
|
|
@ -1430,6 +1434,7 @@ private function parseAttachmentDescription(string $description): array
|
|||
'maps_embed_url' => null,
|
||||
'exif_datetime' => null,
|
||||
'device' => null,
|
||||
'capture_source' => null,
|
||||
'file_label' => null,
|
||||
'original_label' => null,
|
||||
'other' => [],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,299 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="mx-auto max-w-5xl 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 inviare</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">Ticket Acqua Light</h2>
|
||||
<div class="mt-1 text-sm text-slate-600">Flusso rapido per studio e collaboratori: cerca l'unità, conferma il nominativo, scatta la foto e registra la lettura.</div>
|
||||
</div>
|
||||
<div class="rounded-full bg-amber-50 px-3 py-1 text-xs font-semibold text-amber-800">Versione rapida affiancata alla completa</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-[0.95fr_1.05fr]">
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="grid gap-4">
|
||||
<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">Servizio acqua</span>
|
||||
<select wire:model.live="servizioId" class="w-full rounded-lg border-gray-300">
|
||||
<option value="">Seleziona servizio</option>
|
||||
@foreach($servizioOptions as $option)
|
||||
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Cerca nominativo / unità</span>
|
||||
<input type="text" wire:model.live.debounce.250ms="unitaSearch" class="w-full rounded-lg border-gray-300" placeholder="Es. Maggiore, Int. 10, B_4..." />
|
||||
</label>
|
||||
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Unità immobiliare</span>
|
||||
<select wire:model.live="unitaId" class="w-full rounded-lg border-gray-300">
|
||||
<option value="">Seleziona unità</option>
|
||||
@foreach($this->filteredUnitaOptions 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 letturista</span>
|
||||
<input type="text" wire:model.defer="rilevatoreNome" class="w-full rounded-lg border-gray-300" placeholder="Nominativo che invia la lettura" />
|
||||
@if(filled($suggestedReaderName))
|
||||
<div class="mt-1 text-[11px] text-slate-500">Proposto dall'unità: {{ $suggestedReaderName }}</div>
|
||||
@endif
|
||||
</label>
|
||||
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Contatto</span>
|
||||
<input type="text" wire:model.defer="rilevatoreContatto" class="w-full rounded-lg border-gray-300" placeholder="Telefono o email" />
|
||||
</label>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
||||
<div class="text-center text-xl font-semibold text-slate-900">Riportare solo i numeri neri</div>
|
||||
<div class="mx-auto mt-4 aspect-square max-w-[18rem] rounded-full border-[6px] border-slate-400 bg-white p-6">
|
||||
<div class="flex items-end justify-center gap-1 pt-8">
|
||||
@for($i = 0; $i < 5; $i++)
|
||||
<div class="h-14 w-10 rounded-sm border-2 border-slate-700 bg-white"></div>
|
||||
@endfor
|
||||
<div class="pb-1 text-2xl font-semibold text-slate-900">m³</div>
|
||||
</div>
|
||||
<div class="mt-8 flex items-center justify-center">
|
||||
<div class="grid grid-cols-2 gap-4 text-red-600">
|
||||
@for($i = 0; $i < 4; $i++)
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-full border-2 border-red-300 bg-white text-3xl font-bold">NO</div>
|
||||
@endfor
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-center text-[11px] text-slate-600">Le immagini che hai fornito funzionano bene come guida visiva: mostrano chiaramente che vanno riportate solo le cifre nere.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 shadow-sm">
|
||||
<div class="text-sm font-semibold text-sky-900">Lettura attuale del contatore</div>
|
||||
<input type="number" step="0.001" min="0" wire:model.defer="letturaAttuale" class="mt-3 w-full rounded-xl border-sky-300 bg-white text-3xl font-semibold text-slate-900" placeholder="Es. 02320" />
|
||||
@if($previousReading)
|
||||
<div class="mt-2 text-[11px] text-sky-800">Ultima lettura nota: {{ number_format((float) $previousReading['value'], 3, ',', '.') }} del {{ $previousReading['date'] ?: 'n/d' }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<label class="block text-sm"
|
||||
x-data="{
|
||||
localWaterPreviews: [],
|
||||
localWaterClientMetadata: [],
|
||||
init() {
|
||||
window.__ticketAcquaLightPreviewState = window.__ticketAcquaLightPreviewState || [];
|
||||
window.__ticketAcquaLightClientMetadataState = window.__ticketAcquaLightClientMetadataState || [];
|
||||
this.localWaterPreviews = [...window.__ticketAcquaLightPreviewState];
|
||||
this.localWaterClientMetadata = [...window.__ticketAcquaLightClientMetadataState];
|
||||
this.syncClientMetadataToWire();
|
||||
},
|
||||
persist() {
|
||||
window.__ticketAcquaLightPreviewState = [...this.localWaterPreviews];
|
||||
},
|
||||
persistClientMetadata() {
|
||||
window.__ticketAcquaLightClientMetadataState = [...this.localWaterClientMetadata];
|
||||
this.syncClientMetadataToWire();
|
||||
},
|
||||
revoke(items) {
|
||||
items.forEach((item) => {
|
||||
if (item.url) {
|
||||
URL.revokeObjectURL(item.url);
|
||||
}
|
||||
});
|
||||
},
|
||||
syncClientMetadataToWire() {
|
||||
$wire.set('waterClientMetadata', this.localWaterClientMetadata.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.localWaterClientMetadata.map((item) => [`${item.name}|${item.size}`, item]));
|
||||
items.forEach((item) => {
|
||||
if (!item?.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
existing.set(`${item.name}|${item.size}`, item);
|
||||
});
|
||||
this.localWaterClientMetadata = 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.localWaterPreviews.map((item) => [item.key, item]));
|
||||
mapped.forEach((item) => existing.set(item.key, item));
|
||||
this.localWaterPreviews = 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.localWaterPreviews.find((item) => item.name === name && Number(item.size || 0) === Number(size || 0));
|
||||
|
||||
return exact?.url || null;
|
||||
},
|
||||
clearAll() {
|
||||
this.revoke(this.localWaterPreviews);
|
||||
this.localWaterPreviews = [];
|
||||
this.localWaterClientMetadata = [];
|
||||
this.persist();
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
}"
|
||||
x-on:ticket-acqua-draft-reset.window="clearAll()"
|
||||
>
|
||||
<span class="mb-2 block font-medium">Foto contatore</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="pendingWaterPhotos" multiple accept="image/*" capture="environment" class="hidden" x-on:change="addFiles($event)" />
|
||||
</label>
|
||||
<div class="mt-1 text-xs text-gray-500">Massimo 4 foto. Se la camera taglia EXIF, salvo comunque GPS/data dal browser quando disponibili.</div>
|
||||
|
||||
@if(count($this->selectedWaterUploads) > 0)
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
@foreach($this->selectedWaterUploads 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>
|
||||
@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);
|
||||
$exifDate = trim((string) data_get($metadata, 'exif_datetime', ''));
|
||||
@endphp
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
@if(($metadata['capture_source'] ?? null) === 'camera')
|
||||
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-800">GPS da browser</span>
|
||||
@elseif(($metadata['capture_source'] ?? null) === 'attachment')
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-[10px] font-semibold text-indigo-800">EXIF da libreria</span>
|
||||
@endif
|
||||
@if($hasGps)
|
||||
<span class="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-semibold text-emerald-800">GPS</span>
|
||||
@endif
|
||||
@if($exifDate !== '')
|
||||
<span class="inline-flex items-center rounded-full bg-sky-100 px-2 py-0.5 text-[10px] font-semibold text-sky-800">EXIF</span>
|
||||
@endif
|
||||
</div>
|
||||
<label class="mt-2 block">
|
||||
<span class="mb-1 block text-[11px] font-medium">Nota sotto la foto</span>
|
||||
<input type="text" wire:model.defer="waterPhotoDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. lettura chiara" />
|
||||
</label>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</label>
|
||||
</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 generali</span>
|
||||
<textarea rows="3" wire:model.defer="noteGenerali" class="w-full rounded-lg border-gray-300" placeholder="Annotazioni rapide per lo studio..."></textarea>
|
||||
</label>
|
||||
<div class="mt-3">
|
||||
<x-filament::button wire:click="registraLetturaAcqua" wire:loading.attr="disabled" wire:target="registraLetturaAcqua,pendingWaterPhotos">
|
||||
<span wire:loading.remove wire:target="registraLetturaAcqua">Registra lettura light</span>
|
||||
<span wire:loading wire:target="registraLetturaAcqua">Salvataggio...</span>
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
@ -287,6 +287,13 @@
|
|||
$device = trim((string) data_get($metadata, 'device', ''));
|
||||
@endphp
|
||||
@if($hasGps || $exifDate !== '' || $device !== '')
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
@if(($metadata['capture_source'] ?? null) === 'camera')
|
||||
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-800">GPS da browser</span>
|
||||
@elseif(($metadata['capture_source'] ?? null) === 'attachment')
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-[10px] font-semibold text-indigo-800">EXIF da libreria</span>
|
||||
@endif
|
||||
</div>
|
||||
<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>
|
||||
|
|
@ -297,6 +304,9 @@
|
|||
@if($device !== '')
|
||||
<div><span class="font-medium">Dispositivo:</span> {{ $device }}</div>
|
||||
@endif
|
||||
@if(!empty($metadata['capture_source']))
|
||||
<div><span class="font-medium">Sorgente metadati:</span> {{ $metadata['capture_source'] === 'camera' ? 'browser/camera live' : $metadata['capture_source'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<label class="mt-2 block">
|
||||
|
|
|
|||
|
|
@ -356,6 +356,11 @@
|
|||
@if(!empty($details['gps']))
|
||||
<span class="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-semibold text-emerald-800">GPS</span>
|
||||
@endif
|
||||
@if(($details['capture_source'] ?? null) === 'camera')
|
||||
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-800">GPS da browser</span>
|
||||
@elseif(($details['capture_source'] ?? null) === 'attachment')
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-[10px] font-semibold text-indigo-800">EXIF da libreria</span>
|
||||
@endif
|
||||
@if(!empty($details['exif_datetime']))
|
||||
<span class="inline-flex items-center rounded-full bg-sky-100 px-2 py-0.5 text-[10px] font-semibold text-sky-800">EXIF</span>
|
||||
@endif
|
||||
|
|
@ -374,6 +379,9 @@
|
|||
@if(!empty($details['device']))
|
||||
<div><span class="font-medium">Dispositivo:</span> {{ $details['device'] }}</div>
|
||||
@endif
|
||||
@if(!empty($details['capture_source']))
|
||||
<div><span class="font-medium">Sorgente metadati:</span> {{ $details['capture_source'] === 'camera' ? 'browser/camera live' : $details['capture_source'] }}</div>
|
||||
@endif
|
||||
@if(!empty($details['file_label']))
|
||||
<div><span class="font-medium">Nome file:</span> {{ $details['file_label'] }}</div>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -498,6 +498,11 @@
|
|||
@if($hasGps)
|
||||
<span class="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-semibold text-emerald-800">GPS</span>
|
||||
@endif
|
||||
@if(($metadata['capture_source'] ?? null) === 'camera')
|
||||
<span class="inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-[10px] font-semibold text-amber-800">GPS da browser</span>
|
||||
@elseif(($metadata['capture_source'] ?? null) === 'attachment')
|
||||
<span class="inline-flex items-center rounded-full bg-indigo-100 px-2 py-0.5 text-[10px] font-semibold text-indigo-800">EXIF da libreria</span>
|
||||
@endif
|
||||
@if($exifDate !== '')
|
||||
<span class="inline-flex items-center rounded-full bg-sky-100 px-2 py-0.5 text-[10px] font-semibold text-sky-800">EXIF</span>
|
||||
@endif
|
||||
|
|
@ -518,6 +523,9 @@
|
|||
@if($device !== '')
|
||||
<div><span class="font-medium">Dispositivo:</span> {{ $device }}</div>
|
||||
@endif
|
||||
@if(!empty($metadata['capture_source']))
|
||||
<div><span class="font-medium">Sorgente metadati:</span> {{ $metadata['capture_source'] === 'camera' ? 'browser/camera live' : $metadata['capture_source'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<label class="mt-2 block">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user