365 lines
26 KiB
PHP
365 lines
26 KiB
PHP
<x-filament-panels::page>
|
|
@php($ticket = $this->intervento->ticket)
|
|
|
|
<div class="space-y-4"
|
|
x-data="{
|
|
localFotoPreviews: [],
|
|
localDocumentoPreviews: [],
|
|
revoke(items) { (items || []).forEach(item => { if (item && item.url) { URL.revokeObjectURL(item.url); } }); },
|
|
mapFiles(fileList) { return Array.from(fileList || []).map(file => ({ name: file.name, size: file.size || 0, isImage: (file.type || '').startsWith('image/'), url: (file.type || '').startsWith('image/') ? URL.createObjectURL(file) : null })); },
|
|
setPreviews(event, type) { const key = type === 'foto' ? 'localFotoPreviews' : 'localDocumentoPreviews'; this.revoke(this[key]); this[key] = this.mapFiles(event.target.files); },
|
|
clearPreviews(type = null) { if (type === null || type === 'foto') { this.revoke(this.localFotoPreviews); this.localFotoPreviews = []; } if (type === null || type === 'documenti') { this.revoke(this.localDocumentoPreviews); this.localDocumentoPreviews = []; } },
|
|
formatSize(bytes) { if (!bytes) return '0 KB'; const units = ['B', 'KB', 'MB', 'GB']; let value = bytes; let unitIndex = 0; while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024; unitIndex++; } return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`; }
|
|
}"
|
|
x-on:fornitore-local-previews-clear.window="clearPreviews()">
|
|
<div class="rounded-xl border bg-white p-4">
|
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<div class="text-lg font-semibold">Intervento ticket #{{ $this->intervento->ticket_id }}</div>
|
|
<div class="mt-1 text-sm text-gray-600">{{ $ticket->titolo ?? '-' }} · {{ $ticket->stabile->denominazione ?? '-' }}</div>
|
|
<div class="mt-2 text-xs text-gray-500">Fornitore: {{ $this->fornitore->ragione_sociale ?: trim(($this->fornitore->nome ?? '') . ' ' . ($this->fornitore->cognome ?? '')) }}</div>
|
|
</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
<a href="{{ $this->getTicketsUrl() }}" class="inline-flex items-center rounded-md bg-gray-800 px-3 py-1.5 text-xs font-medium text-white hover:bg-gray-700">Torna ai ticket</a>
|
|
<a href="{{ $this->getCollaboratoriUrl() }}" class="inline-flex items-center rounded-md bg-primary-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-primary-500">Collaboratori</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid gap-4 xl:grid-cols-3">
|
|
<div class="space-y-4 xl:col-span-2">
|
|
<div class="rounded-xl border bg-white p-4">
|
|
<div class="text-sm font-semibold">Scheda segnalazione</div>
|
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-gray-500">Richiedente</div>
|
|
<div class="mt-1 text-sm">{{ $caller['contatto'] ?: '-' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-gray-500">Telefono</div>
|
|
<div class="mt-1 text-sm">
|
|
@if($caller['telefono'] !== '')
|
|
<a href="tel:{{ preg_replace('/\s+/', '', (string) $caller['telefono']) }}" class="text-primary-600 hover:underline">{{ $caller['telefono'] }}</a>
|
|
@else
|
|
-
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-gray-500">Email</div>
|
|
<div class="mt-1 text-sm">
|
|
@if($caller['email'] !== '')
|
|
<a href="mailto:{{ $caller['email'] }}" class="text-primary-600 hover:underline">{{ $caller['email'] }}</a>
|
|
@else
|
|
-
|
|
@endif
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-gray-500">Origine dato</div>
|
|
<div class="mt-1 text-sm">{{ $caller['sorgente'] !== '' ? $caller['sorgente'] : '-' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-gray-500">Stato intervento</div>
|
|
<div class="mt-1 text-sm">{{ $this->intervento->stato }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-gray-500">Operatore</div>
|
|
<div class="mt-1 text-sm">{{ $this->intervento->operatore_assegnato_label }}</div>
|
|
</div>
|
|
<div class="md:col-span-2">
|
|
<div class="text-xs uppercase tracking-wide text-gray-500">Riferimento stabile</div>
|
|
<div class="mt-1 text-sm">{{ $caller['riferimento'] !== '' ? $caller['riferimento'] : '-' }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-sm text-gray-700">{{ $ticket->descrizione }}</div>
|
|
</div>
|
|
|
|
<div class="rounded-xl border bg-white p-4">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<div class="text-sm font-semibold">Operativo e rapportino</div>
|
|
@if($this->intervento->stato === 'assegnato')
|
|
<x-filament::button size="sm" wire:click="startIntervento">Prendi in carico</x-filament::button>
|
|
@endif
|
|
</div>
|
|
|
|
@if($this->canAssignDipendente())
|
|
<div class="mt-4 rounded-lg border border-dashed p-3">
|
|
<div class="text-xs uppercase tracking-wide text-gray-500">Assegnazione operatore</div>
|
|
<div class="mt-2 flex flex-wrap items-end gap-3">
|
|
<label class="block min-w-72 flex-1 text-sm">
|
|
<span class="mb-1 block font-medium">Collaboratore incaricato</span>
|
|
<select wire:model="dipendenteId" class="w-full rounded-lg border-gray-300">
|
|
<option value="">Responsabile fornitore / non assegnato</option>
|
|
@foreach($dipendentiOptions as $option)
|
|
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
|
@endforeach
|
|
</select>
|
|
</label>
|
|
<x-filament::button color="gray" wire:click="assignDipendente">Salva assegnazione</x-filament::button>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="mt-4 grid gap-3 md:grid-cols-3">
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Marca apparato</span>
|
|
<input type="text" wire:model.defer="apparatoMarca" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Modello</span>
|
|
<input type="text" wire:model.defer="apparatoModello" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Seriale</span>
|
|
<input type="text" wire:model.defer="apparatoSeriale" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
</div>
|
|
|
|
<label class="mt-4 block text-sm">
|
|
<span class="mb-1 block font-medium">Rapporto intervento</span>
|
|
<textarea wire:model.defer="rapportoFornitore" rows="6" class="w-full rounded-lg border-gray-300"></textarea>
|
|
</label>
|
|
|
|
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Tempo impiegato (minuti)</span>
|
|
<input type="number" min="1" max="1440" wire:model.defer="tempoMinuti" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">QR token</span>
|
|
<input type="text" wire:model.defer="qrToken" class="w-full rounded-lg border-gray-300" placeholder="Inserisci il codice se scansionato" />
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mt-4 grid gap-2 md:grid-cols-3">
|
|
@foreach($articoliUtilizzati as $index => $item)
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Ricambio/attivita {{ $index + 1 }}</span>
|
|
<input type="text" wire:model.defer="articoliUtilizzati.{{ $index }}" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
|
|
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Foto lavoro</span>
|
|
<input type="file" wire:model="fotoLavoro" multiple accept="image/*" x-on:change="setPreviews($event, 'foto')" class="w-full rounded-lg border-gray-300 text-sm" />
|
|
</label>
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Documenti lavoro</span>
|
|
<input type="file" wire:model="documentiLavoro" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,image/*" x-on:change="setPreviews($event, 'documenti')" class="w-full rounded-lg border-gray-300 text-sm" />
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mt-4 rounded-xl border border-dashed border-sky-200 bg-sky-50/60 p-4" x-show="localFotoPreviews.length || localDocumentoPreviews.length" x-cloak>
|
|
<div class="text-sm font-semibold text-sky-900">Anteprima immediata allegati fornitore</div>
|
|
<div class="mt-1 text-xs text-sky-700">Le immagini e i documenti selezionati sono visibili subito prima del salvataggio del rapportino.</div>
|
|
|
|
<div class="mt-4" x-show="localFotoPreviews.length" x-cloak>
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-sky-800">Foto lavoro</div>
|
|
<div class="mt-2 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
<template x-for="item in localFotoPreviews" :key="item.name + item.size">
|
|
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
|
|
<div class="aspect-[4/3] overflow-hidden rounded-lg border bg-slate-50">
|
|
<template x-if="item.url">
|
|
<img :src="item.url" :alt="item.name" class="h-full w-full object-cover" />
|
|
</template>
|
|
</div>
|
|
<div class="mt-2 truncate font-medium" x-text="item.name"></div>
|
|
<div class="text-gray-500" x-text="formatSize(item.size)"></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4" x-show="localDocumentoPreviews.length" x-cloak>
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-sky-800">Documenti lavoro</div>
|
|
<div class="mt-2 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
<template x-for="item in localDocumentoPreviews" :key="item.name + item.size">
|
|
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
|
|
<div class="flex aspect-[4/3] items-center justify-center rounded-lg border bg-slate-50">
|
|
<template x-if="item.url">
|
|
<img :src="item.url" :alt="item.name" class="h-full w-full rounded object-cover" />
|
|
</template>
|
|
<template x-if="!item.url">
|
|
<div class="text-center text-gray-500">
|
|
<div class="text-sm font-semibold">FILE</div>
|
|
<div class="mt-1 text-[11px]">Anteprima pronta</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
<div class="mt-2 truncate font-medium" x-text="item.name"></div>
|
|
<div class="text-gray-500" x-text="formatSize(item.size)"></div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 grid gap-2 md:grid-cols-2">
|
|
@foreach($descrizioneFile as $index => $item)
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Descrizione file {{ $index + 1 }}</span>
|
|
<input type="text" wire:model.defer="descrizioneFile.{{ $index }}" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
@endforeach
|
|
</div>
|
|
|
|
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Messaggio veloce allo studio</span>
|
|
<textarea wire:model.defer="messaggioVeloce" rows="3" class="w-full rounded-lg border-gray-300"></textarea>
|
|
</label>
|
|
<div class="grid gap-3">
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Codice proforma</span>
|
|
<input type="text" wire:model.defer="proformaCodice" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block text-sm">
|
|
<span class="mb-1 block font-medium">Note proforma</span>
|
|
<textarea wire:model.defer="proformaNote" rows="3" class="w-full rounded-lg border-gray-300"></textarea>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 flex flex-wrap gap-4 text-sm">
|
|
<label class="inline-flex items-center gap-2">
|
|
<input type="checkbox" wire:model="richiestaProforma" class="rounded border-gray-300" />
|
|
<span>Richiesta proforma</span>
|
|
</label>
|
|
<label class="inline-flex items-center gap-2">
|
|
<input type="checkbox" wire:model="lavoroStandard" class="rounded border-gray-300" />
|
|
<span>Lavoro standard gia autorizzato</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
<x-filament::button wire:click="saveRapporto">Salva avanzamento</x-filament::button>
|
|
<x-filament::button color="gray" wire:click="inviaSollecito">Invia sollecito</x-filament::button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-xl border bg-white p-4">
|
|
<div class="text-sm font-semibold">Comunicazioni ticket</div>
|
|
<div class="mt-3 space-y-2">
|
|
@forelse($ticket->messages as $msg)
|
|
<div class="rounded-lg border p-3 text-sm">
|
|
<div class="font-medium">{{ $msg->user->name ?? 'Sistema' }}</div>
|
|
<div class="mt-1 whitespace-pre-wrap text-gray-700">{{ $msg->messaggio }}</div>
|
|
<div class="mt-1 text-xs text-gray-500">{{ optional($msg->created_at)->format('d/m/Y H:i') }}</div>
|
|
</div>
|
|
@empty
|
|
<div class="text-sm text-gray-500">Nessuna comunicazione disponibile.</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div class="rounded-xl border bg-white p-4">
|
|
<div class="text-sm font-semibold">Stato operativo</div>
|
|
<div class="mt-3 space-y-2 text-sm">
|
|
<div><span class="font-medium">Stato:</span> {{ $this->intervento->stato }}</div>
|
|
<div><span class="font-medium">Iniziato:</span> {{ optional($this->intervento->iniziato_at)->format('d/m/Y H:i') ?: '-' }}</div>
|
|
<div><span class="font-medium">Terminato:</span> {{ optional($this->intervento->terminato_at)->format('d/m/Y H:i') ?: '-' }}</div>
|
|
<div><span class="font-medium">Rif. chiusura:</span> {{ $this->intervento->riferimento_chiusura ?: '-' }}</div>
|
|
<div><span class="font-medium">QR registrato:</span> {{ $this->intervento->qr_token ?: '-' }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-xl border bg-white p-4">
|
|
<div class="text-sm font-semibold">Allegati ticket</div>
|
|
<div class="mt-3 grid gap-3 sm:grid-cols-2">
|
|
@forelse($ticket->attachments as $attachment)
|
|
<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">
|
|
@if($attachment->isImage())
|
|
<img src="{{ $this->getAttachmentPublicUrl($attachment) }}" alt="{{ $attachment->original_file_name }}" class="h-full w-full rounded object-cover" />
|
|
@else
|
|
<div class="flex h-full items-center justify-center text-center text-gray-500">
|
|
<div>
|
|
<div class="text-sm font-semibold">{{ strtoupper(pathinfo((string) $attachment->original_file_name, PATHINFO_EXTENSION) ?: ($attachment->isPdf() ? 'PDF' : 'FILE')) }}</div>
|
|
<div class="mt-1 text-[11px]">Anteprima disponibile in modal</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="mt-2 font-medium">{{ $attachment->original_file_name }}</div>
|
|
<div class="mt-1 text-gray-600">{{ $attachment->description ?: 'Nessuna descrizione' }}</div>
|
|
<div class="mt-2 text-[11px] text-gray-500">Caricato da {{ $attachment->user->name ?? 'Sistema' }} il {{ optional($attachment->created_at)->format('d/m/Y H:i') ?: '-' }}</div>
|
|
<button type="button" wire:click="openAttachmentPreview({{ (int) $attachment->id }})" class="mt-2 inline-flex items-center rounded-md bg-indigo-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-indigo-500">Visualizza in modal</button>
|
|
</div>
|
|
@empty
|
|
<div class="text-sm text-gray-500">Nessun allegato disponibile. Se il ticket e stato creato senza file persistiti, qui non verra mostrato nulla finche non vengono caricati allegati reali.</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-xl border bg-white p-4">
|
|
<div class="text-sm font-semibold">Storico correlato</div>
|
|
<div class="mt-3 space-y-2">
|
|
@forelse($storicoRows as $row)
|
|
<div class="rounded-lg border p-3 text-sm">
|
|
<div class="font-medium">#{{ $row['ticket_id'] }} - {{ $row['titolo'] }}</div>
|
|
<div class="text-xs text-gray-500">{{ $row['stato'] }} · {{ $row['updated_at'] }}</div>
|
|
@if($row['note'] !== '')
|
|
<div class="mt-1 text-xs text-gray-600">{{ \Illuminate\Support\Str::limit($row['note'], 90) }}</div>
|
|
@endif
|
|
</div>
|
|
@empty
|
|
<div class="text-sm text-gray-500">Nessuno storico correlato.</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if($attachmentPreview)
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" x-data="{ zoom: 1, pdfPage: 1, iframeKey: 0, refreshPdf() { this.iframeKey++; }, zoomIn() { this.zoom = Math.min(this.zoom + 0.2, 4); }, zoomOut() { this.zoom = Math.max(this.zoom - 0.2, 0.5); }, resetZoom() { this.zoom = 1; }, nextPage() { this.pdfPage = this.pdfPage + 1; this.refreshPdf(); }, prevPage() { this.pdfPage = Math.max(this.pdfPage - 1, 1); this.refreshPdf(); }, printPdf() { const frame = this.$refs.pdfFrame; if (frame?.contentWindow) { frame.contentWindow.focus(); frame.contentWindow.print(); } else { window.open(@js($attachmentPreview['url']), '_blank'); } } }">
|
|
<div class="h-[92vh] w-full max-w-[96vw] rounded-xl bg-white shadow-2xl">
|
|
<div class="flex items-center justify-between border-b px-4 py-3">
|
|
<div>
|
|
<div class="text-sm font-semibold">{{ $attachmentPreview['name'] ?? 'Allegato' }}</div>
|
|
@if(!empty($attachmentPreview['description']))
|
|
<div class="text-xs text-gray-500">{{ $attachmentPreview['description'] }}</div>
|
|
@endif
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
@if($attachmentPreview['is_image'] ?? false)
|
|
<button type="button" x-on:click="zoomOut()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">-</button>
|
|
<button type="button" x-on:click="resetZoom()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">100%</button>
|
|
<button type="button" x-on:click="zoomIn()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">+</button>
|
|
@elseif($attachmentPreview['is_pdf'] ?? false)
|
|
<button type="button" x-on:click="prevPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina precedente</button>
|
|
<span class="text-xs text-slate-500">Pagina <span x-text="pdfPage"></span></span>
|
|
<button type="button" x-on:click="nextPage()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Pagina successiva</button>
|
|
<button type="button" x-on:click="printPdf()" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Stampa PDF</button>
|
|
@endif
|
|
<a href="{{ $attachmentPreview['url'] }}" target="_blank" class="rounded-md bg-slate-100 px-2 py-1 text-xs hover:bg-slate-200">Apri in nuova scheda</a>
|
|
<button type="button" wire:click="closeAttachmentPreview" class="rounded-md bg-gray-100 px-2 py-1 text-xs hover:bg-gray-200">Chiudi</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="h-[calc(92vh-64px)] overflow-auto p-4">
|
|
@if($attachmentPreview['is_image'] ?? false)
|
|
<div class="h-full overflow-auto rounded-lg bg-slate-50 p-4">
|
|
<div class="flex min-h-full min-w-full items-start justify-center">
|
|
<img src="{{ $attachmentPreview['url'] }}" alt="Anteprima allegato" class="max-w-none rounded border bg-white shadow" x-bind:style="`width: ${zoom * 100}%; height: auto;`" />
|
|
</div>
|
|
</div>
|
|
@elseif($attachmentPreview['is_pdf'] ?? false)
|
|
<iframe x-ref="pdfFrame" x-bind:key="iframeKey" x-bind:src="@js($attachmentPreview['url']) + '#toolbar=1&navpanes=0&scrollbar=1&view=FitH&zoom=page-width&page=' + pdfPage" class="h-[84vh] w-full rounded border"></iframe>
|
|
@else
|
|
<div class="rounded-md border bg-gray-50 p-4 text-sm text-gray-700">
|
|
Anteprima non disponibile per questo formato. Scarica o apri il file:
|
|
<a href="{{ $attachmentPreview['url'] }}" class="ml-1 text-primary-600 hover:underline">Apri file</a>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</x-filament-panels::page> |