Fix Ticket Mobile attachment metadata and unified cards

This commit is contained in:
michele 2026-04-12 19:15:53 +00:00
parent 1301c4b144
commit c4f5bdcb72
4 changed files with 96 additions and 60 deletions

View File

@ -384,7 +384,37 @@ public function closeAttachmentMapPreview(): void
*/ */
public function getAttachmentDetails(TicketAttachment $attachment): array public function getAttachmentDetails(TicketAttachment $attachment): array
{ {
return $this->parseAttachmentDescription((string) ($attachment->description ?? '')); $details = $this->parseAttachmentDescription((string) ($attachment->description ?? ''));
$metadata = is_array($attachment->metadata ?? null) ? $attachment->metadata : [];
$lat = $metadata['gps_decimal']['lat'] ?? null;
$lng = $metadata['gps_decimal']['lng'] ?? null;
if ($details['gps'] === null && is_numeric($lat) && is_numeric($lng)) {
$details['gps'] = [
'lat' => (float) $lat,
'lng' => (float) $lng,
'label' => number_format((float) $lat, 5, '.', '') . ', ' . number_format((float) $lng, 5, '.', ''),
];
$details['maps_embed_url'] = 'https://maps.google.com/maps?q=' . $lat . ',' . $lng . '&z=17&output=embed';
}
if ($details['maps_url'] === null && filled($metadata['google_maps_url'] ?? null)) {
$details['maps_url'] = trim((string) $metadata['google_maps_url']);
}
if ($details['exif_datetime'] === null && filled($metadata['exif_datetime'] ?? null)) {
$details['exif_datetime'] = trim((string) $metadata['exif_datetime']);
}
if ($details['device'] === null && filled($metadata['device'] ?? null)) {
$details['device'] = trim((string) $metadata['device']);
}
if ($details['file_label'] === null && filled($metadata['stored_name'] ?? null)) {
$details['file_label'] = trim((string) $metadata['stored_name']);
}
return $details;
} }
public function getSelectedTicketProperty(): ?Ticket public function getSelectedTicketProperty(): ?Ticket

View File

@ -683,7 +683,7 @@ public function removeNewTicketFile(int $index): void
} }
/** /**
* @return array<int, array{index:int,name:string,original_name:string,protocol:string,mime:string,is_image:bool,preview_url:?string,description:string}> * @return array<int, array{index:int,name:string,original_name:string,protocol:string,mime:string,is_image:bool,preview_url:?string,description:string,size:int,metadata:array<string,mixed>}>
*/ */
public function getSelectedUploadsProperty(): array public function getSelectedUploadsProperty(): array
{ {
@ -699,6 +699,10 @@ public function getSelectedUploadsProperty(): array
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream'); $mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
$isImage = str_starts_with($mime, 'image/'); $isImage = str_starts_with($mime, 'image/');
$protocol = $this->resolveDraftUploadCode($file, (int) $index, $isImage); $protocol = $this->resolveDraftUploadCode($file, (int) $index, $isImage);
$size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0);
$metadata = $isImage
? app(TicketAttachmentUploadService::class)->inspectUpload($file)
: [];
$previewUrl = null; $previewUrl = null;
if ($isImage && method_exists($file, 'temporaryUrl')) { if ($isImage && method_exists($file, 'temporaryUrl')) {
@ -718,6 +722,8 @@ public function getSelectedUploadsProperty(): array
'is_image' => $isImage, 'is_image' => $isImage,
'preview_url' => $previewUrl, 'preview_url' => $previewUrl,
'description' => (string) ($this->newTicketAttachmentDescriptions[$index] ?? ''), 'description' => (string) ($this->newTicketAttachmentDescriptions[$index] ?? ''),
'size' => $size,
'metadata' => is_array($metadata) ? $metadata : [],
]; ];
} }
@ -744,6 +750,7 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
} }
$saved = 0; $saved = 0;
$supportsAttachmentMetadata = Schema::hasColumn('ticket_attachments', 'metadata');
$files = array_merge($this->newTicketCameraShots, $this->newTicketAttachments); $files = array_merge($this->newTicketCameraShots, $this->newTicketAttachments);
@ -759,7 +766,7 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
'display_name' => $displayName, 'display_name' => $displayName,
]); ]);
TicketAttachment::query()->create([ $payload = [
'ticket_id' => (int) $ticket->id, 'ticket_id' => (int) $ticket->id,
'ticket_update_id' => null, 'ticket_update_id' => null,
'user_id' => $userId, 'user_id' => $userId,
@ -773,7 +780,13 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
$stored['original_name'] ?? null, $stored['original_name'] ?? null,
$stored['source_original_name'] ?? null, $stored['source_original_name'] ?? null,
), ),
]); ];
if ($supportsAttachmentMetadata) {
$payload['metadata'] = is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [];
}
TicketAttachment::query()->create($payload);
$saved++; $saved++;
} catch (Throwable $e) { } catch (Throwable $e) {

View File

@ -9,6 +9,15 @@
class TicketAttachmentUploadService class TicketAttachmentUploadService
{ {
/**
* @param UploadedFile|TemporaryUploadedFile $file
* @return array<string,mixed>
*/
public function inspectUpload(object $file): array
{
return $this->extractImageMetadataFromFileObject($file);
}
/** /**
* @param UploadedFile|TemporaryUploadedFile $file * @param UploadedFile|TemporaryUploadedFile $file
* @return array{path:string,mime:string,size:int,original_name:string} * @return array{path:string,mime:string,size:int,original_name:string}

View File

@ -233,11 +233,6 @@
<textarea rows="3" wire:model.defer="newTicketDescrizione" class="w-full rounded-lg border-gray-300" placeholder="Descrivi il problema senza ripetere i dati tecnici della chiamata..." ></textarea> <textarea rows="3" wire:model.defer="newTicketDescrizione" class="w-full rounded-lg border-gray-300" placeholder="Descrivi il problema senza ripetere i dati tecnici della chiamata..." ></textarea>
</label> </label>
<label class="block text-sm md:col-span-2">
<span class="mb-1 block font-medium">Nota generale foto/documentazione (opzionale)</span>
<textarea rows="2" wire:model.defer="newTicketFotoNote" class="w-full rounded-lg border-gray-300" placeholder="Es. perdita visibile sotto il lavello, foto allegata" ></textarea>
</label>
<label class="block text-sm md:col-span-2" <label class="block text-sm md:col-span-2"
x-data="{ x-data="{
draftProtocol: @js($newTicketDraftProtocol), draftProtocol: @js($newTicketDraftProtocol),
@ -362,57 +357,9 @@
<div class="mt-1 text-xs text-gray-500">iPhone: su "Aggiungi allegati" scegli "Sfoglia" per aprire l'app File (iCloud Drive/On My iPhone).</div> <div class="mt-1 text-xs text-gray-500">iPhone: su "Aggiungi allegati" scegli "Sfoglia" per aprire l'app File (iCloud Drive/On My iPhone).</div>
<div wire:loading wire:target="pendingTicketCameraShots,pendingTicketAttachments" class="mt-2 text-xs font-medium text-amber-700">Caricamento in corso: sto accodando foto e allegati alla bozza ticket.</div> <div wire:loading wire:target="pendingTicketCameraShots,pendingTicketAttachments" class="mt-2 text-xs font-medium text-amber-700">Caricamento in corso: sto accodando foto e allegati alla bozza ticket.</div>
<div x-show="(localCameraPreviews.length || localAttachmentPreviews.length) && !@js(count($this->selectedUploads) > 0)" x-cloak class="mt-3 rounded-xl border border-sky-200 bg-sky-50 p-3"> <div x-show="localCameraPreviews.length || localAttachmentPreviews.length" x-cloak class="mt-3 rounded-xl border border-sky-200 bg-sky-50 p-3">
<div class="text-xs font-semibold text-sky-900">Anteprima immediata sul telefono</div> <div class="text-xs font-semibold text-sky-900">Preparazione allegati in corso</div>
<div class="mt-1 text-[11px] text-sky-800">Questa anteprima resta visibile solo finche i file non entrano nella coda operativa. Appena sono accodati, miniatura e descrizione restano nello stesso box qui sotto.</div> <div class="mt-1 text-[11px] text-sky-800">Le foto e i file stanno entrando nella coda operativa qui sotto. Mantengo un solo box finale per ogni allegato, con miniatura e descrizione nello stesso riquadro.</div>
<template x-if="localCameraPreviews.length">
<div class="mt-3">
<div class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-sky-900">Foto appena scattate</div>
<div class="grid gap-3 sm:grid-cols-2">
<template x-for="item in localCameraPreviews" :key="item.key">
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
<div class="aspect-[4/3] rounded-lg border bg-gray-50 p-2">
<img x-show="item.url" :src="item.url" :alt="item.name" class="h-full w-full rounded object-cover" />
</div>
<div class="mt-2 font-medium" x-text="item.name"></div>
<div class="mt-1 text-[11px] font-semibold text-emerald-700" x-text="`Nome bozza: ${item.draftName}`"></div>
<div class="mt-1 text-[11px] text-sky-700">In attesa di accodamento</div>
<div class="mt-1 text-[11px] text-gray-500" x-text="formatSize(item.size)"></div>
</div>
</template>
</div>
</div>
</template>
<template x-if="localAttachmentPreviews.length">
<div class="mt-3">
<div class="mb-2 text-[11px] font-semibold uppercase tracking-wide text-sky-900">Allegati appena selezionati</div>
<div class="grid gap-3 sm:grid-cols-2">
<template x-for="item in localAttachmentPreviews" :key="item.key">
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
<div class="aspect-[4/3] rounded-lg border bg-gray-50 p-2">
<template x-if="item.isImage && item.url">
<img :src="item.url" :alt="item.name" class="h-full w-full rounded object-cover" />
</template>
<template x-if="!item.isImage">
<div class="flex h-full items-center justify-center text-center text-gray-500">
<div>
<div class="text-sm font-semibold" x-text="(item.name.split('.').pop() || 'FILE').toUpperCase()"></div>
<div class="mt-1 text-[11px]">Pronto per l'invio</div>
</div>
</div>
</template>
</div>
<div class="mt-2 font-medium" x-text="item.name"></div>
<div class="mt-1 text-[11px] font-semibold text-emerald-700" x-text="`Nome bozza: ${item.draftName}`"></div>
<div class="mt-1 text-[11px] text-sky-700">In attesa di accodamento</div>
<div class="mt-1 text-[11px] text-gray-500" x-text="formatSize(item.size)"></div>
</div>
</template>
</div>
</div>
</template>
</div> </div>
</label> </label>
@ -442,6 +389,43 @@
@if($upload['original_name'] !== $upload['name']) @if($upload['original_name'] !== $upload['name'])
<div class="mt-1 text-[11px] text-gray-500">File origine: {{ $upload['original_name'] }}</div> <div class="mt-1 text-[11px] text-gray-500">File origine: {{ $upload['original_name'] }}</div>
@endif @endif
<div class="mt-1 text-[11px] text-gray-500">{{ number_format(((int) ($upload['size'] ?? 0)) / 1024, 1, ',', '.') }} KB</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', ''));
$device = trim((string) data_get($metadata, 'device', ''));
$mapsUrl = trim((string) data_get($metadata, 'google_maps_url', ''));
@endphp
@if($hasGps || $exifDate !== '' || $device !== '')
<div class="mt-2 flex flex-wrap gap-1">
@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
@if($device !== '')
<span class="inline-flex items-center rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-700">{{ $device }}</span>
@endif
</div>
<div class="mt-2 rounded-lg border border-slate-200 bg-slate-50 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($mapsUrl !== '')
<div class="mt-1 break-all"><span class="font-medium">Maps:</span> {{ $mapsUrl }}</div>
@endif
@if($exifDate !== '')
<div><span class="font-medium">Data EXIF:</span> {{ $exifDate }}</div>
@endif
@if($device !== '')
<div><span class="font-medium">Dispositivo:</span> {{ $device }}</div>
@endif
</div>
@endif
<label class="mt-2 block"> <label class="mt-2 block">
<span class="mb-1 block text-[11px] font-medium">Descrizione foto/allegato</span> <span class="mb-1 block text-[11px] font-medium">Descrizione foto/allegato</span>
<input type="text" wire:model.defer="newTicketAttachmentDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. perdita in cucina lato sifone" /> <input type="text" wire:model.defer="newTicketAttachmentDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. perdita in cucina lato sifone" />