785 lines
63 KiB
PHP
785 lines
63 KiB
PHP
<x-filament-panels::page>
|
|
@php
|
|
$ticket = $this->intervento->ticket;
|
|
@endphp
|
|
|
|
<div class="space-y-4"
|
|
x-data="{
|
|
activeTab: 'scheda',
|
|
localFotoPreviews: [],
|
|
localDocumentoPreviews: [],
|
|
geoBusy: false,
|
|
revoke(items) { (items || []).forEach(item => { if (item && item.url) { URL.revokeObjectURL(item.url); } }); },
|
|
mapFiles(fileList) {
|
|
return Array.from(fileList || []).map(file => {
|
|
const mime = file.type || '';
|
|
const isImage = mime.startsWith('image/');
|
|
const isPdf = mime === 'application/pdf' || (file.name || '').toLowerCase().endsWith('.pdf');
|
|
return {
|
|
name: file.name,
|
|
size: file.size || 0,
|
|
isImage,
|
|
isPdf,
|
|
url: (isImage || isPdf) ? 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]}`; },
|
|
captureGeo(action) {
|
|
return new Promise((resolve) => {
|
|
if (!navigator.geolocation) {
|
|
resolve({ source: 'browser', error: `geolocation_not_supported_${action}` });
|
|
return;
|
|
}
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => resolve({
|
|
source: 'browser',
|
|
lat: position.coords?.latitude ?? null,
|
|
lng: position.coords?.longitude ?? null,
|
|
accuracy: position.coords?.accuracy ?? null,
|
|
action,
|
|
}),
|
|
(error) => resolve({ source: 'browser', error: error?.message || `geolocation_failed_${action}`, action }),
|
|
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 60000 },
|
|
);
|
|
});
|
|
},
|
|
async runGeoAction(action) {
|
|
this.geoBusy = true;
|
|
const payload = await this.captureGeo(action);
|
|
if (action === 'start') {
|
|
await $wire.startInterventoWithGeo(payload);
|
|
} else if (action === 'stop') {
|
|
await $wire.stopInterventoWithGeo(payload);
|
|
} else if (action === 'checkpoint') {
|
|
await $wire.addPresenceCheckpoint(payload);
|
|
}
|
|
this.geoBusy = false;
|
|
}
|
|
}"
|
|
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="{{ \App\Filament\Pages\Fornitore\LavorazioniOperative::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Lavorazioni</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>
|
|
<a href="{{ $this->getRubricaUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Rubrica clienti</a>
|
|
<a href="{{ \App\Filament\Pages\Fornitore\ImpostazioniArchivio::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament') }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Impostazioni</a>
|
|
<a href="{{ $this->getProdottiUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Prodotti</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-xl border border-slate-200 bg-slate-50 p-3">
|
|
<div class="flex flex-wrap items-center gap-2">
|
|
<button type="button" x-on:click="activeTab = 'scheda'" x-bind:class="activeTab === 'scheda' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Segnalazione</button>
|
|
<button type="button" x-on:click="activeTab = 'operativo'" x-bind:class="activeTab === 'operativo' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Operativo</button>
|
|
<button type="button" x-on:click="activeTab = 'prodotti'" x-bind:class="activeTab === 'prodotti' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Prodotti</button>
|
|
<button type="button" x-on:click="activeTab = 'allegati'" x-bind:class="activeTab === 'allegati' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Allegati</button>
|
|
<button type="button" x-on:click="activeTab = 'storico'" x-bind:class="activeTab === 'storico' ? 'bg-slate-900 text-white ring-slate-900' : 'bg-white text-slate-700 ring-slate-300'" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-semibold ring-1 ring-inset transition">Storico</button>
|
|
<div class="ml-auto text-[11px] uppercase tracking-wide text-slate-500">Vista compatta TecnoRepair-like</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" x-show="activeTab === 'scheda'" x-cloak>
|
|
<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">
|
|
@if(!empty($caller['rubrica_url']))
|
|
<a href="{{ $caller['rubrica_url'] }}" class="text-primary-600 hover:underline">{{ $caller['contatto'] ?: '-' }}</a>
|
|
@else
|
|
{{ $caller['contatto'] ?: '-' }}
|
|
@endif
|
|
</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">
|
|
@if(!empty($caller['stabile_url']) && $caller['riferimento'] !== '')
|
|
<a href="{{ $caller['stabile_url'] }}" class="text-primary-600 hover:underline">{{ $caller['riferimento'] }}</a>
|
|
@else
|
|
{{ $caller['riferimento'] !== '' ? $caller['riferimento'] : '-' }}
|
|
@endif
|
|
</div>
|
|
@if($caller['unita_label'] !== '')
|
|
<div class="mt-1 text-xs text-gray-500">Unità collegata: {{ $caller['unita_label'] }}</div>
|
|
@endif
|
|
</div>
|
|
@if(!empty($caller['rubrica_url']) || !empty($caller['estratto_url']) || !empty($caller['stabile_url']))
|
|
<div class="md:col-span-2">
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
@if(!empty($caller['rubrica_url']))
|
|
<a href="{{ $caller['rubrica_url'] }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Apri scheda contatto</a>
|
|
@endif
|
|
@if(!empty($caller['estratto_url']))
|
|
<a href="{{ $caller['estratto_url'] }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Apri estratto conto</a>
|
|
@endif
|
|
@if(!empty($caller['stabile_url']))
|
|
<a href="{{ $caller['stabile_url'] }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Apri stabile</a>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
<div class="mt-4 whitespace-pre-wrap rounded-lg bg-gray-50 p-3 text-sm text-gray-700">{{ $ticket->descrizione }}</div>
|
|
</div>
|
|
|
|
@if(count($accessSummaryRows) > 0 || filled($dispatchBoardNote))
|
|
<div class="rounded-xl border border-amber-200 bg-amber-50/60 p-4" x-show="activeTab === 'scheda'" x-cloak>
|
|
<div class="text-sm font-semibold text-amber-900">Accesso, chiavi e reperibilità</div>
|
|
@if(filled($dispatchBoardNote))
|
|
<div class="mt-2 rounded-lg border border-amber-200 bg-white/80 p-3 text-xs text-amber-900">{{ $dispatchBoardNote }}</div>
|
|
@endif
|
|
|
|
<div class="mt-3 space-y-3">
|
|
@foreach($accessSummaryRows as $row)
|
|
<div class="rounded-xl border bg-white p-4 text-sm shadow-sm">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<div class="font-medium text-slate-900">{{ $row['stabile_label'] }}</div>
|
|
@if($row['is_priority'])
|
|
<span class="rounded-full bg-amber-100 px-2 py-1 text-[11px] font-semibold text-amber-800">Prioritario</span>
|
|
@endif
|
|
</div>
|
|
<div class="mt-3 grid gap-3 md:grid-cols-2">
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-slate-500">Chi suonare</div>
|
|
<div class="mt-1">{{ $row['chi_suonare'] !== '' ? $row['chi_suonare'] : '-' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-slate-500">Referente</div>
|
|
<div class="mt-1">{{ $row['contatto_nome'] !== '' ? $row['contatto_nome'] : '-' }}</div>
|
|
@if($row['contatto_telefono'] !== '')
|
|
<a href="tel:{{ preg_replace('/\s+/', '', (string) $row['contatto_telefono']) }}" class="mt-1 inline-flex text-xs font-medium text-primary-600 hover:underline">{{ $row['contatto_telefono'] }}</a>
|
|
@endif
|
|
</div>
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-slate-500">Dove sono le chiavi</div>
|
|
<div class="mt-1">{{ $row['dove_sono_chiavi'] !== '' ? $row['dove_sono_chiavi'] : '-' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs uppercase tracking-wide text-slate-500">Fascia reperibilità</div>
|
|
<div class="mt-1">{{ $row['fascia_reperibilita'] !== '' ? $row['fascia_reperibilita'] : '-' }}</div>
|
|
</div>
|
|
@if($row['istruzioni_accesso'] !== '')
|
|
<div class="md:col-span-2">
|
|
<div class="text-xs uppercase tracking-wide text-slate-500">Istruzioni accesso</div>
|
|
<div class="mt-1 whitespace-pre-wrap text-slate-700">{{ $row['istruzioni_accesso'] }}</div>
|
|
</div>
|
|
@endif
|
|
@if($row['note_urgenza'] !== '')
|
|
<div class="md:col-span-2 rounded-lg border border-rose-200 bg-rose-50 p-3 text-xs text-rose-900">
|
|
<span class="font-semibold">Nota urgenza:</span> {{ $row['note_urgenza'] }}
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="rounded-xl border bg-white p-4" x-show="activeTab === 'operativo'" x-cloak>
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<div class="text-sm font-semibold">Operativo e rapportino</div>
|
|
<div class="flex flex-wrap gap-2">
|
|
@if($this->intervento->stato === 'assegnato' || ! $this->intervento->iniziato_at || $this->intervento->terminato_at)
|
|
<x-filament::button size="sm" x-on:click.prevent="runGeoAction('start')" x-bind:disabled="geoBusy">Start intervento</x-filament::button>
|
|
@endif
|
|
@if($this->intervento->iniziato_at && ! $this->intervento->terminato_at)
|
|
<x-filament::button size="sm" color="gray" x-on:click.prevent="runGeoAction('stop')" x-bind:disabled="geoBusy">Stop intervento</x-filament::button>
|
|
@endif
|
|
<x-filament::button size="sm" color="info" x-on:click.prevent="runGeoAction('checkpoint')" x-bind:disabled="geoBusy">Checkpoint posizione</x-filament::button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-2 text-[11px] text-slate-500">Start, stop e checkpoint provano a salvare la posizione del browser. Se il GPS non e disponibile o negato, l'evento viene comunque registrato senza coordinate.</div>
|
|
|
|
<div class="mt-3 grid gap-3 rounded-xl border border-slate-200 bg-slate-50 p-3 text-xs text-slate-700 md:grid-cols-3">
|
|
<div>
|
|
<div class="font-semibold uppercase tracking-wide text-slate-500">Timer avvio</div>
|
|
<div class="mt-1">{{ optional($this->intervento->iniziato_at)->format('d/m/Y H:i') ?: 'Non avviato' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold uppercase tracking-wide text-slate-500">Timer stop</div>
|
|
<div class="mt-1">{{ optional($this->intervento->terminato_at)->format('d/m/Y H:i') ?: 'In corso' }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="font-semibold uppercase tracking-wide text-slate-500">Durata rilevata</div>
|
|
<div class="mt-1">{{ $tempoMinuti ? ($tempoMinuti . ' minuti') : 'Da calcolare' }}</div>
|
|
</div>
|
|
</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 rounded-xl border border-emerald-200 bg-emerald-50 p-4">
|
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
|
<div>
|
|
<div class="text-sm font-semibold text-emerald-900">Materiali e prodotti</div>
|
|
<div class="mt-1 text-xs text-emerald-800">Puoi cercare per nome, codice interno o barcode. I lettori Bluetooth che si comportano come tastiera funzionano gia su questo campo.</div>
|
|
</div>
|
|
<a href="{{ $this->getProdottiUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-emerald-800 ring-1 ring-inset ring-emerald-300 hover:bg-emerald-100">Apri catalogo prodotti</a>
|
|
</div>
|
|
|
|
<div class="mt-3 flex flex-col gap-3 md:flex-row">
|
|
<label class="block flex-1 text-sm">
|
|
<span class="mb-1 block font-medium">Cerca prodotto o scansiona barcode</span>
|
|
<input type="text" wire:model.live.debounce.300ms="materialSearch" inputmode="search" class="w-full rounded-lg border-emerald-300" placeholder="Es. lampadina E27 10W oppure barcode" />
|
|
</label>
|
|
<div class="flex items-end gap-2">
|
|
<x-filament::button color="success" wire:click="addManualMaterial">Aggiungi manuale</x-filament::button>
|
|
</div>
|
|
</div>
|
|
|
|
@if(count($preferredMaterialRows) > 0)
|
|
<div class="mt-4 rounded-xl border border-emerald-200 bg-white p-4">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-emerald-700">Preferiti fornitore</div>
|
|
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
|
@foreach($preferredMaterialRows as $index => $row)
|
|
<div class="rounded-xl border border-emerald-100 bg-emerald-50/40 p-3 text-sm shadow-sm">
|
|
<div class="font-medium">{{ $row['label'] }}</div>
|
|
@if($row['brand_model'] !== '')
|
|
<div class="mt-1 text-xs text-slate-500">{{ $row['brand_model'] }}</div>
|
|
@endif
|
|
<div class="mt-1 text-xs text-slate-500">Barcode: {{ $row['barcode'] !== '' ? $row['barcode'] : 'n/d' }} · Qty tipica: {{ rtrim(rtrim(number_format((float) $row['default_qty'], 2, '.', ''), '0'), '.') }}</div>
|
|
<div class="mt-1 text-xs text-slate-500">Fonte: {{ $row['preferred_source'] !== '' ? $row['preferred_source'] : 'Memoria fornitore' }}@if($row['last_price'] !== null) · Prezzo: {{ number_format((float) $row['last_price'], 2, ',', '.') }} {{ $row['currency'] }}@endif</div>
|
|
@if($row['note'] !== '')
|
|
<div class="mt-2 text-xs text-slate-600">{{ $row['note'] }}</div>
|
|
@endif
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
<button type="button" wire:click="addPreferredMaterial({{ $index }})" class="inline-flex items-center rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-500">Aggiungi al rapportino</button>
|
|
@if($row['preferred_url'] !== '')
|
|
<a href="{{ $row['preferred_url'] }}" target="_blank" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Apri riferimento</a>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if(count($historicalMaterialRows) > 0)
|
|
<div class="mt-4 rounded-xl border border-sky-200 bg-white p-4">
|
|
<div class="text-xs font-semibold uppercase tracking-wide text-sky-700">Storico materiali usati</div>
|
|
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
|
@foreach($historicalMaterialRows as $index => $row)
|
|
<div class="rounded-xl border border-sky-100 bg-sky-50/40 p-3 text-sm shadow-sm">
|
|
<div class="font-medium">{{ $row['label'] }}</div>
|
|
<div class="mt-1 text-xs text-slate-500">Usato {{ $row['usage_count'] }} volte · ultimo ticket #{{ $row['ticket_id'] }} · {{ $row['last_used_at'] }}</div>
|
|
<div class="mt-1 text-xs text-slate-500">Barcode: {{ $row['barcode'] !== '' ? $row['barcode'] : 'n/d' }}@if($row['preferred_source'] !== '') · Fonte: {{ $row['preferred_source'] }}@endif</div>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
<button type="button" wire:click="addHistoricalMaterial({{ $index }})" class="inline-flex items-center rounded-md bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-500">Riusa nel rapportino</button>
|
|
@if($row['preferred_url'] !== '')
|
|
<a href="{{ $row['preferred_url'] }}" target="_blank" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Apri riferimento</a>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
@if(count($materialSuggestions) > 0)
|
|
<div class="mt-3 grid gap-3 lg:grid-cols-2">
|
|
@foreach($materialSuggestions as $suggestion)
|
|
<div class="rounded-xl border bg-white p-3 text-sm shadow-sm">
|
|
<div class="font-medium">{{ $suggestion['label'] }}</div>
|
|
<div class="mt-1 text-xs text-gray-500">Barcode: {{ $suggestion['barcode'] !== '' ? $suggestion['barcode'] : 'non presente' }}</div>
|
|
<div class="mt-1 text-xs text-gray-500">Prezzo: {{ $suggestion['price'] ?: 'non disponibile' }} · Fonte: {{ $suggestion['source'] }}</div>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
<button type="button" wire:click="addSuggestedMaterial({{ (int) $suggestion['id'] }})" class="inline-flex items-center rounded-md bg-emerald-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-emerald-500">Aggiungi al rapportino</button>
|
|
@if(!empty($suggestion['external_url']))
|
|
<a href="{{ $suggestion['external_url'] }}" target="_blank" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Apri fonte</a>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@elseif($materialSearch !== '')
|
|
<div class="mt-3 rounded-lg border border-dashed border-amber-300 bg-amber-50 p-3 text-xs text-amber-800">
|
|
Nessun prodotto trovato nel catalogo corrente. Puoi inserirlo manualmente adesso e usare il link Amazon di ricerca nel rapportino, poi completare l'anagrafica prodotto dal catalogo.
|
|
</div>
|
|
@endif
|
|
|
|
@if(count($materialiUtilizzati) > 0)
|
|
<div class="mt-4 space-y-3">
|
|
@foreach($materialiUtilizzati as $index => $materiale)
|
|
<div class="rounded-xl border bg-white p-3 text-sm shadow-sm">
|
|
<div class="grid gap-3 md:grid-cols-4">
|
|
<label class="block md:col-span-2">
|
|
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">Prodotto/materiale</span>
|
|
<input type="text" wire:model.defer="materialiUtilizzati.{{ $index }}.descrizione" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block">
|
|
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">Barcode</span>
|
|
<input type="text" wire:model.defer="materialiUtilizzati.{{ $index }}.barcode" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block">
|
|
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">Quantita</span>
|
|
<input type="number" step="0.01" min="0.01" wire:model.defer="materialiUtilizzati.{{ $index }}.qty" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block">
|
|
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">Prezzo nostro</span>
|
|
<input type="number" step="0.01" min="0" wire:model.defer="materialiUtilizzati.{{ $index }}.unit_price" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block">
|
|
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">Valuta</span>
|
|
<input type="text" wire:model.defer="materialiUtilizzati.{{ $index }}.currency" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
<label class="block md:col-span-2">
|
|
<span class="mb-1 block text-xs font-medium uppercase tracking-wide text-gray-500">Fonte</span>
|
|
<input type="text" wire:model.defer="materialiUtilizzati.{{ $index }}.source_label" class="w-full rounded-lg border-gray-300" />
|
|
</label>
|
|
</div>
|
|
|
|
<div class="mt-3 flex flex-wrap items-center justify-between gap-2">
|
|
@if(!empty($materiale['external_url']))
|
|
<a href="{{ $materiale['external_url'] }}" target="_blank" class="text-xs font-medium text-primary-600 hover:underline">Apri riferimento esterno</a>
|
|
@else
|
|
<span class="text-xs text-gray-400">Nessun riferimento esterno</span>
|
|
@endif
|
|
<div class="flex flex-wrap gap-2">
|
|
<button type="button" wire:click="saveCurrentMaterialAsPreferred({{ (int) $index }})" class="inline-flex items-center rounded-md bg-slate-100 px-2 py-1 text-[11px] font-medium text-slate-700 hover:bg-slate-200">Memorizza</button>
|
|
<button type="button" wire:click="removeMateriale({{ (int) $index }})" class="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>
|
|
</div>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
@endif
|
|
</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.isImage && item.url">
|
|
<img :src="item.url" :alt="item.name" class="h-full w-full rounded object-cover" />
|
|
</template>
|
|
<template x-if="item.isPdf && item.url">
|
|
<iframe :src="item.url + '#toolbar=0&navpanes=0&scrollbar=0&page=1&view=FitH'" class="h-full w-full rounded border-0 bg-white"></iframe>
|
|
</template>
|
|
<template x-if="!item.isImage && !item.isPdf">
|
|
<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>
|
|
|
|
@if(count($this->rapportinoUploads) > 0)
|
|
<div class="mt-4 rounded-xl border bg-white p-4">
|
|
<div class="text-sm font-semibold">File selezionati per il rapportino</div>
|
|
<div class="mt-1 text-xs text-gray-500">Ogni foto o documento puo avere la sua descrizione, come nella gestione ticket.</div>
|
|
|
|
<div class="mt-3 grid gap-3 sm:grid-cols-2">
|
|
@foreach($this->rapportinoUploads as $upload)
|
|
<div class="rounded-xl border bg-slate-50 p-3 text-xs shadow-sm">
|
|
<div class="aspect-[4/3] rounded-lg border bg-white p-2">
|
|
@if($upload['is_image'] && $upload['preview_url'])
|
|
<img src="{{ $upload['preview_url'] }}" alt="{{ $upload['name'] }}" class="h-full w-full rounded object-cover" />
|
|
@elseif($upload['is_pdf'])
|
|
<div class="flex h-full items-center justify-center text-center text-gray-500">
|
|
<div>
|
|
<div class="text-sm font-semibold">PDF</div>
|
|
<div class="mt-1 text-[11px]">Anteprima immediata disponibile sopra durante la selezione</div>
|
|
</div>
|
|
</div>
|
|
@else
|
|
<div class="flex h-full items-center justify-center text-center text-gray-500">
|
|
<div>
|
|
<div class="text-sm font-semibold">FILE</div>
|
|
<div class="mt-1 text-[11px]">{{ strtoupper(pathinfo((string) $upload['name'], PATHINFO_EXTENSION) ?: 'DOC') }}</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="mt-2 font-medium">{{ $upload['name'] }}</div>
|
|
<div class="mt-1 text-gray-500">{{ number_format(((int) $upload['size']) / 1024, 1, ',', '.') }} KB</div>
|
|
|
|
<label class="mt-3 block text-sm">
|
|
<span class="mb-1 block font-medium">Descrizione allegato</span>
|
|
<input type="text" wire:model.defer="descrizioneFile.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. foto finale, documento tecnico, rapporto PDF" />
|
|
</label>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<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" x-show="activeTab === 'operativo'" x-cloak>
|
|
<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" x-show="activeTab === 'operativo'" x-cloak>
|
|
<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" x-show="activeTab === 'operativo'" x-cloak>
|
|
<div class="flex items-center justify-between gap-2">
|
|
<div class="text-sm font-semibold">Sessioni e presenza</div>
|
|
<div class="text-xs text-gray-500">{{ count($sessionRows) }} eventi</div>
|
|
</div>
|
|
|
|
@if($lastGeoSnapshot)
|
|
<div class="mt-3 rounded-lg border border-sky-200 bg-sky-50 p-3 text-xs text-sky-900">
|
|
<div class="font-semibold">{{ $lastGeoSnapshot['label'] }}</div>
|
|
<div class="mt-1">Start: {{ $lastGeoSnapshot['start_geo'] }}</div>
|
|
<div class="mt-1">Stop: {{ $lastGeoSnapshot['end_geo'] }}</div>
|
|
<div class="mt-1 text-sky-700">Aggiornato: {{ $lastGeoSnapshot['updated_at'] }}</div>
|
|
</div>
|
|
@endif
|
|
|
|
<div class="mt-3 space-y-2">
|
|
@forelse($sessionRows as $row)
|
|
<div class="rounded-lg border p-3 text-xs">
|
|
<div class="flex items-center justify-between gap-2">
|
|
<span class="inline-flex rounded-full bg-slate-100 px-2 py-1 font-semibold text-slate-700">{{ $row['type'] === 'checkpoint' ? 'Checkpoint' : 'Sessione lavoro' }}</span>
|
|
<span class="text-gray-500">{{ $row['duration_minutes'] !== null ? ($row['duration_minutes'] . ' min') : '-' }}</span>
|
|
</div>
|
|
<div class="mt-2 text-gray-700">{{ $row['note'] !== '' ? $row['note'] : 'Nessuna nota' }}</div>
|
|
<div class="mt-2 text-gray-500">Start: {{ $row['started_at'] }} · {{ $row['start_geo'] }}</div>
|
|
<div class="mt-1 text-gray-500">Stop: {{ $row['ended_at'] }} · {{ $row['end_geo'] }}</div>
|
|
</div>
|
|
@empty
|
|
<div class="text-sm text-gray-500">Nessuna sessione registrata.</div>
|
|
@endforelse
|
|
</div>
|
|
</div>
|
|
|
|
<div class="rounded-xl border bg-white p-4" x-show="activeTab === 'operativo'" x-cloak>
|
|
<div class="text-sm font-semibold">Google workspace</div>
|
|
@if($googleWorkspaceStatus)
|
|
<div class="mt-3 space-y-2 text-sm">
|
|
<div><span class="font-medium">Stato:</span> {{ $googleWorkspaceStatus['connected'] ? 'Collegato' : 'Non collegato' }}</div>
|
|
<div><span class="font-medium">Account:</span> {{ $googleWorkspaceStatus['email'] !== '' ? $googleWorkspaceStatus['email'] : '-' }}</div>
|
|
<div><span class="font-medium">Nome:</span> {{ $googleWorkspaceStatus['name'] !== '' ? $googleWorkspaceStatus['name'] : '-' }}</div>
|
|
<div><span class="font-medium">Ultimo collegamento:</span> {{ $googleWorkspaceStatus['connected_at'] !== '' ? $googleWorkspaceStatus['connected_at'] : '-' }}</div>
|
|
</div>
|
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
<a href="{{ $this->getGoogleConnectUrl() }}" class="inline-flex items-center rounded-md bg-white px-3 py-1.5 text-xs font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Collega / aggiorna Google</a>
|
|
@if($googleWorkspaceStatus['connected'])
|
|
<form method="POST" action="{{ $this->getGoogleDisconnectUrl() }}">
|
|
@csrf
|
|
<button type="submit" class="inline-flex items-center rounded-md bg-rose-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-rose-500">Scollega</button>
|
|
</form>
|
|
@endif
|
|
</div>
|
|
<div class="mt-2 text-xs text-gray-500">Questo e il primo aggancio operativo per riusare contatti, calendario e Drive nello spazio fornitore con lo stesso account Google gia gestito dal backend.</div>
|
|
@else
|
|
<div class="mt-3 text-sm text-gray-500">Nessun contesto amministratore collegato al fornitore: collegamento Google non disponibile.</div>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="space-y-4" x-show="activeTab === 'prodotti'" x-cloak>
|
|
@if(count($stabileInstalledProductRows) > 0)
|
|
@include('filament.components.catalog.product-hub-panel', [
|
|
'title' => 'Prodotti già installati nello stabile',
|
|
'description' => 'Riepilogo calcolato dai ticket/interventi già registrati per questo stabile. Serve per riusare lampade, ricambi e codici coerenti con lo storico.',
|
|
'rows' => $stabileInstalledProductRows,
|
|
'emptyMessage' => 'Nessun prodotto installato rilevato nello storico ticket di questo stabile.',
|
|
'showStableUsage' => true,
|
|
])
|
|
@endif
|
|
|
|
@include('filament.components.catalog.product-hub-panel', [
|
|
'title' => 'Hub catalogo per questo ticket',
|
|
'description' => 'Catalogo fornitore riusabile dal ticket: prezzi correnti, ultime righe acquisto, giacenza nota e riferimento Amazon quando disponibile.',
|
|
'rows' => $ticketProductHubRows,
|
|
'emptyMessage' => 'Nessun prodotto catalogato disponibile per questo fornitore.',
|
|
'showStableUsage' => true,
|
|
])
|
|
</div>
|
|
|
|
<div class="rounded-xl border bg-white p-4" x-show="activeTab === 'allegati'" x-cloak>
|
|
<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" />
|
|
@elseif($attachment->isPdf())
|
|
<iframe src="{{ $this->getAttachmentPublicUrl($attachment) }}#toolbar=0&navpanes=0&scrollbar=0&page=1&view=FitH" class="h-full w-full rounded border-0 bg-white"></iframe>
|
|
@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" x-show="activeTab === 'storico'" x-cloak>
|
|
<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>
|
|
@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
|
|
</div>
|
|
</x-filament-panels::page> |