Fix Ticket Mobile attachment metadata and unified cards
This commit is contained in:
parent
1301c4b144
commit
c4f5bdcb72
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user