feat(fornitore): tabbed operational workspace and quick reminders

This commit is contained in:
michele 2026-03-12 16:55:52 +00:00
parent 62f8613f9e
commit 84215abe73
6 changed files with 413 additions and 59 deletions

View File

@ -4,9 +4,12 @@
use App\Http\Controllers\Controller;
use App\Models\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\TicketAttachment;
use App\Models\TicketIntervento;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
class TicketOperativoController extends Controller
{
@ -30,6 +33,9 @@ public function index(Request $request)
$interventi = $query->paginate(15)->withQueryString();
$rows = collect($interventi->items())
->map(fn(TicketIntervento $intervento): array => $this->buildInterventoRow($intervento));
$fatturabili = TicketIntervento::query()
->where('fornitore_id', $fornitore->id)
->whereIn('stato', ['fatturabile', 'fatturato'])
@ -40,6 +46,7 @@ public function index(Request $request)
return view('fornitore.tickets.index', [
'fornitore' => $fornitore,
'interventi' => $interventi,
'rows' => $rows,
'stato' => $stato,
'fatturabili' => $fatturabili,
]);
@ -50,11 +57,36 @@ public function show(TicketIntervento $intervento)
[$fornitore] = $this->resolveOperatoreContext();
$this->authorizeIntervento($intervento, $fornitore);
$intervento->load(['ticket.stabile', 'ticket.messages.user', 'eseguitoDaDipendente']);
$intervento->load([
'ticket.stabile',
'ticket.messages.user',
'ticket.attachments.user',
'ticket.apertoDaUser',
'ticket.assegnatoAUser',
'ticket.assegnatoAFornitore',
'eseguitoDaDipendente',
]);
$storico = TicketIntervento::query()
->with(['ticket'])
->where('fornitore_id', $fornitore->id)
->where('id', '!=', $intervento->id)
->when(
(int) ($intervento->ticket?->soggetto_richiedente_id ?? 0) > 0,
fn($q) => $q->whereHas('ticket', fn($t) => $t->where('soggetto_richiedente_id', (int) $intervento->ticket->soggetto_richiedente_id)),
fn($q) => $q->whereHas('ticket', fn($t) => $t->where('stabile_id', (int) ($intervento->ticket?->stabile_id ?? 0)))
)
->latest('id')
->limit(12)
->get();
$caller = $this->extractCallerData((string) ($intervento->ticket?->descrizione ?? ''));
return view('fornitore.tickets.show', [
'fornitore' => $fornitore,
'intervento' => $intervento,
'storico' => $storico,
'caller' => $caller,
]);
}
@ -86,15 +118,38 @@ public function complete(Request $request, TicketIntervento $intervento)
'tempo_minuti' => 'nullable|integer|min:1|max:1440',
'articoli_utilizzati' => 'nullable|array',
'articoli_utilizzati.*' => 'nullable|string|max:120',
'foto_lavoro' => 'nullable|image|max:5120',
'foto_lavoro' => 'nullable|array|max:10',
'foto_lavoro.*' => 'nullable|image|max:10240',
'documenti_lavoro' => 'nullable|array|max:10',
'documenti_lavoro.*' => 'nullable|file|max:10240|mimes:pdf,doc,docx,xls,xlsx,txt,jpg,jpeg,png,webp',
'descrizione_file' => 'nullable|array',
'descrizione_file.*' => 'nullable|string|max:255',
'apparato_marca' => 'nullable|string|max:120',
'apparato_modello' => 'nullable|string|max:120',
'apparato_seriale' => 'nullable|string|max:120',
'lavoro_standard' => 'nullable|boolean',
'richiesta_proforma' => 'nullable|boolean',
'proforma_codice' => 'nullable|string|max:80',
'proforma_note' => 'nullable|string|max:2000',
'messaggio_veloce' => 'nullable|string|max:1500',
'qr_token' => 'nullable|string|max:40',
]);
$rapporto = trim((string) $validated['rapporto_fornitore']);
$apparato = $this->buildApparatoSummary(
(string) ($validated['apparato_marca'] ?? ''),
(string) ($validated['apparato_modello'] ?? ''),
(string) ($validated['apparato_seriale'] ?? '')
);
if ($apparato !== '') {
$rapporto .= "\n\n" . $apparato;
}
$payload = [
'rapporto_fornitore' => $validated['rapporto_fornitore'],
'rapporto_fornitore' => $rapporto,
'tempo_minuti' => $validated['tempo_minuti'] ?? null,
'terminato_at' => now(),
'stato' => 'in_verifica',
'stato' => ! empty($validated['lavoro_standard']) ? 'fatturabile' : 'in_verifica',
'eseguito_da_dipendente_id' => $dipendente?->id ?? $intervento->eseguito_da_dipendente_id,
];
@ -108,9 +163,40 @@ public function complete(Request $request, TicketIntervento $intervento)
$payload['articoli_utilizzati'] = $articoli;
}
if ($request->hasFile('foto_lavoro')) {
$payload['foto_path'] = $request->file('foto_lavoro')
->store('ticket-interventi/foto/' . $intervento->id, 'public');
$fotoFiles = $request->file('foto_lavoro', []);
if (! is_array($fotoFiles)) {
$fotoFiles = $fotoFiles ? [$fotoFiles] : [];
}
if (count($fotoFiles) > 0) {
$payload['foto_path'] = $fotoFiles[0]->store('ticket-interventi/foto/' . $intervento->id, 'public');
}
$docFiles = $request->file('documenti_lavoro', []);
if (! is_array($docFiles)) {
$docFiles = $docFiles ? [$docFiles] : [];
}
$allFiles = array_merge($fotoFiles, $docFiles);
$descriptions = (array) ($validated['descrizione_file'] ?? []);
if (Schema::hasTable('ticket_attachments')) {
foreach ($allFiles as $index => $file) {
if (! $file) {
continue;
}
$path = $file->store('ticket-interventi/allegati/' . $intervento->id, 'public');
TicketAttachment::query()->create([
'ticket_id' => (int) $intervento->ticket_id,
'ticket_update_id' => null,
'user_id' => (int) Auth::id(),
'file_path' => $path,
'original_file_name' => (string) $file->getClientOriginalName(),
'mime_type' => (string) $file->getClientMimeType(),
'size' => (int) $file->getSize(),
'description' => trim((string) ($descriptions[$index] ?? '')) ?: 'Allegato rapporto fornitore',
]);
}
}
$tokenInserito = trim((string) ($validated['qr_token'] ?? ''));
@ -120,18 +206,59 @@ public function complete(Request $request, TicketIntervento $intervento)
$intervento->update($payload);
$stamp = now()->format('d/m/Y H:i');
$intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => 'Rapporto intervento caricato dal fornitore.',
'messaggio' => '[' . $stamp . '] Rapporto intervento caricato dal fornitore.',
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
$proformaCodice = trim((string) ($validated['proforma_codice'] ?? ''));
$proformaNote = trim((string) ($validated['proforma_note'] ?? ''));
if (! empty($validated['richiesta_proforma']) || $proformaCodice !== '' || $proformaNote !== '') {
$intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => '[' . $stamp . '] Proforma fornitore ' . ($proformaCodice !== '' ? ('codice ' . $proformaCodice) : '(senza codice)') . ($proformaNote !== '' ? (' - ' . $proformaNote) : ''),
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
}
$quickMessage = trim((string) ($validated['messaggio_veloce'] ?? ''));
if ($quickMessage !== '') {
$intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => '[' . $stamp . '] Messaggio veloce fornitore: ' . $quickMessage,
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
}
return redirect()->route('fornitore.tickets.show', $intervento)
->with('success', 'Intervento inviato in verifica amministratore.');
}
public function sollecito(TicketIntervento $intervento)
{
[$fornitore] = $this->resolveOperatoreContext();
$this->authorizeIntervento($intervento, $fornitore);
$stamp = now()->format('d/m/Y H:i');
$intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => '[' . $stamp . '] Sollecito fornitore su ticket/intervento in corso.',
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
return back()->with('success', 'Sollecito inviato allo studio amministrativo.');
}
/**
* @return array{0:Fornitore,1:?FornitoreDipendente}
*/
@ -187,4 +314,81 @@ private function authorizeIntervento(TicketIntervento $intervento, Fornitore $fo
{
abort_unless((int) $intervento->fornitore_id === (int) $fornitore->id, 403);
}
/**
* @return array{ingresso:string,contatto:string,telefono:string,problema:string,apparato:string}
*/
private function buildInterventoRow(TicketIntervento $intervento): array
{
$descrizione = (string) ($intervento->ticket->descrizione ?? '');
$caller = $this->extractCallerData($descrizione);
return [
'ingresso' => optional($intervento->created_at)->format('d/m/Y H:i') ?: '-',
'contatto' => $caller['contatto'],
'telefono' => $caller['telefono'],
'problema' => $caller['problema'] !== '' ? $caller['problema'] : ((string) ($intervento->ticket->titolo ?? '-')),
'apparato' => $this->extractApparato((string) ($intervento->rapporto_fornitore ?? '')),
];
}
/**
* @return array{contatto:string,telefono:string,problema:string}
*/
private function extractCallerData(string $descrizione): array
{
$contatto = '-';
$telefono = '';
$problema = '';
$lines = preg_split('/\r\n|\r|\n/', $descrizione) ?: [];
foreach ($lines as $line) {
$trim = trim($line);
if ($problema === '' && $trim !== '') {
$problema = $trim;
}
if (str_starts_with($trim, 'Chiamante selezionato:')) {
$contatto = trim((string) str_replace('Chiamante selezionato:', '', $trim));
}
if (str_starts_with($trim, 'Telefono:')) {
$telefono = trim((string) str_replace('Telefono:', '', $trim));
}
}
return [
'contatto' => $contatto,
'telefono' => $telefono,
'problema' => $problema,
];
}
private function extractApparato(string $rapporto): string
{
foreach ((preg_split('/\r\n|\r|\n/', $rapporto) ?: []) as $line) {
$trim = trim($line);
if (str_starts_with(mb_strtolower($trim), 'apparato:')) {
return trim((string) substr($trim, strlen('apparato:')));
}
}
return '-';
}
private function buildApparatoSummary(string $marca, string $modello, string $seriale): string
{
$marca = trim($marca);
$modello = trim($modello);
$seriale = trim($seriale);
if ($marca === '' && $modello === '' && $seriale === '') {
return '';
}
return 'Apparato: '
. ($marca !== '' ? ('marca=' . $marca . ' ') : '')
. ($modello !== '' ? ('modello=' . $modello . ' ') : '')
. ($seriale !== '' ? ('seriale=' . $seriale) : '');
}
}

View File

@ -24,3 +24,5 @@ # Modifiche NetGescon
| 11/03/2026 | 0.8.x-dev | Ticket Mobile / Allegati | Fix accesso allegati con route protetta `admin.tickets.attachments.view` (stop 403 da URL `/storage`), anteprima in modal e lista allegati stile catalogo con descrizione per file in inserimento mobile (foto + documenti) | [U] |
| 11/03/2026 | 0.8.x-dev | CTI Panasonic / Tracciamento | Aggiunto `trace_id` in risposta incoming/call-ended e log strutturato (`CTI Panasonic trace`) per audit chiamate reali/test, ricerca eventi mancanti e diagnosi `event_id`/telefono/canale mirror | [U] |
| 12/03/2026 | 0.8.x-dev | Supporto / Modifiche / Update | Pagina `admin-filament/supporto/modifiche` resa compatta (font piccolo), aggiunta tabella ultimi commit con descrizione e pulsante per eseguire `netgescon:update --apply` con esito/output e versione corrente visibile | [U] |
| 12/03/2026 | 0.8.x-dev | Fornitori / Ticket Operativi | Estesa area fornitore con elenco pratiche operativo (ingresso/contatto/telefono click-to-call/problema/stato), scheda a TAB con allegati segnalazione, storico interventi correlati, ricambi, note rapide e flusso chiusura/proforma; aggiunto sollecito rapido verso studio | [U] |
| 12/03/2026 | 0.8.x-dev | Supporto Ticket / TAB Fornitori | Sezione "Fornitori attivi su ticket aperti" spostata in Tab 3 dedicata in `supporto/ticket-gestione` per allineare il flusso operativo elenco -> scheda -> fornitori | [U] |

View File

@ -21,6 +21,9 @@
<button type="button" wire:click="$set('activeTab', 'scheda')" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'scheda' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
Tab 2 - Scheda ticket
</button>
<button type="button" wire:click="$set('activeTab', 'fornitori')" class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $activeTab === 'fornitori' ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' }}">
Tab 3 - Fornitori attivi
</button>
</div>
@if($activeTab === 'elenco')
<select wire:model.live="status" class="rounded-lg border-gray-300 text-sm">
@ -97,7 +100,7 @@
</tbody>
</table>
</div>
@elseif($activeTab === 'fornitori')
<div class="mt-4 rounded-xl border bg-white p-4">
<div class="text-sm font-semibold">Fornitori attivi su ticket aperti</div>
<div class="mt-1 text-xs text-gray-500">Mostra solo fornitori con ticket/interventi in corso, con stato operativo e ultimo aggiornamento.</div>

View File

@ -55,33 +55,55 @@
</div>
<div class="card">
<div class="card-header">I miei ticket</div>
<div class="card-header">Pratiche / Interventi / Manutenzioni</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th>Ticket</th>
<th>Stabile</th>
<th>Ingresso</th>
<th>Contatto</th>
<th>Telefono</th>
<th>Problema</th>
<th>Apparato</th>
<th>Stato</th>
<th>Aggiornato</th>
<th></th>
</tr>
</thead>
<tbody>
@forelse($interventi as $intervento)
@forelse($interventi as $idx => $intervento)
@php($row = $rows[$idx] ?? ['ingresso' => '-', 'contatto' => '-', 'telefono' => '', 'problema' => '-', 'apparato' => '-'])
<tr>
<td>#{{ $intervento->ticket_id }} - {{ $intervento->ticket->titolo ?? '-' }}</td>
<td>{{ $intervento->ticket->stabile->denominazione ?? '-' }}</td>
<td>{{ $row['ingresso'] }}</td>
<td>
<div class="fw-semibold">{{ $row['contatto'] }}</div>
<small class="text-muted">Ticket #{{ $intervento->ticket_id }} - {{ $intervento->ticket->stabile->denominazione ?? '-' }}</small>
</td>
<td>
@if(!empty($row['telefono']))
<a href="tel:{{ preg_replace('/\s+/', '', (string) $row['telefono']) }}" class="btn btn-sm btn-outline-success">
{{ $row['telefono'] }}
</a>
@else
<span class="text-muted">-</span>
@endif
</td>
<td>{{ \Illuminate\Support\Str::limit((string) $row['problema'], 90) }}</td>
<td>{{ $row['apparato'] }}</td>
<td><span class="badge bg-info text-dark">{{ $intervento->stato }}</span></td>
<td>{{ optional($intervento->updated_at)->format('d/m/Y H:i') }}</td>
<td>
<a href="{{ route('fornitore.tickets.show', $intervento) }}" class="btn btn-sm btn-outline-primary">Apri</a>
<a href="{{ route('fornitore.tickets.show', $intervento) }}" class="btn btn-sm btn-outline-primary">Scheda</a>
<form method="POST" action="{{ route('fornitore.tickets.sollecito', $intervento) }}" class="d-inline">
@csrf
<button type="submit" class="btn btn-sm btn-outline-warning">Sollecito</button>
</form>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="text-center text-muted py-4">Nessun intervento disponibile.</td>
<td colspan="9" class="text-center text-muted py-4">Nessun intervento disponibile.</td>
</tr>
@endforelse
</tbody>

View File

@ -12,17 +12,98 @@
<a href="{{ route('fornitore.tickets.index') }}" class="btn btn-outline-secondary btn-sm">Torna ai ticket</a>
</div>
<div class="row g-3">
<div class="col-lg-7">
<div class="card mb-3">
<div class="card-header">Note amministratore</div>
<div class="card-body">
<p class="mb-0">{{ $intervento->note_amministratore ?: 'Nessuna nota amministratore.' }}</p>
<div class="card mb-3">
<div class="card-body py-2">
<ul class="nav nav-tabs card-header-tabs" id="fornitoreTicketTabs" role="tablist">
<li class="nav-item" role="presentation"><button class="nav-link active" data-bs-toggle="tab" data-bs-target="#tab-scheda" type="button">1. Scheda Segnalazione</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-operativo" type="button">2. Operativo e Ricambi</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-comunicazioni" type="button">3. Comunicazioni</button></li>
<li class="nav-item" role="presentation"><button class="nav-link" data-bs-toggle="tab" data-bs-target="#tab-chiusura" type="button">4. Chiusura e Proforma</button></li>
</ul>
</div>
</div>
<div class="tab-content">
<div class="tab-pane fade show active" id="tab-scheda">
<div class="row g-3">
<div class="col-lg-8">
<div class="card mb-3">
<div class="card-header">Dati segnalazione</div>
<div class="card-body">
<div class="mb-2"><strong>Richiedente:</strong> {{ $caller['contatto'] ?: '-' }}</div>
<div class="mb-2"><strong>Telefono:</strong>
@if(!empty($caller['telefono']))
<a href="tel:{{ preg_replace('/\s+/', '', (string) $caller['telefono']) }}">{{ $caller['telefono'] }}</a>
@else
-
@endif
</div>
<div class="mb-2"><strong>Aperto da:</strong> {{ $intervento->ticket->apertoDaUser->name ?? '-' }}</div>
<div class="mb-2"><strong>Assegnato a fornitore da:</strong> {{ $intervento->ticket->assegnatoAUser->name ?? '-' }}</div>
<div class="mb-0"><strong>Descrizione:</strong>
<div class="mt-1 text-muted" style="white-space: pre-wrap;">{{ $intervento->ticket->descrizione }}</div>
</div>
</div>
</div>
<div class="card mb-3">
<div class="card-header">Foto e documenti segnalati</div>
<div class="card-body">
@if($intervento->ticket->attachments->isEmpty())
<p class="text-muted mb-0">Nessun allegato disponibile.</p>
@else
<div class="row g-3">
@foreach($intervento->ticket->attachments as $a)
<div class="col-md-6">
<div class="border rounded p-2 h-100">
<div class="small fw-semibold">{{ $a->original_file_name }}</div>
<div class="small text-muted mb-2">{{ $a->description ?: 'Nessuna descrizione' }}</div>
<a class="btn btn-sm btn-outline-primary" href="{{ route('admin.tickets.attachments.view', ['attachment' => (int) $a->id]) }}" target="_blank">Apri</a>
</div>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-3">
<div class="card-header">Stato operativo</div>
<div class="card-body">
<div class="mb-2"><strong>Stato:</strong> <span class="badge bg-info text-dark">{{ $intervento->stato }}</span></div>
<div class="mb-2"><strong>Operatore:</strong> {{ $intervento->eseguitoDaDipendente?->nome_completo ?: 'Responsabile fornitore' }}</div>
<div class="mb-2"><strong>Iniziato:</strong> {{ optional($intervento->iniziato_at)->format('d/m/Y H:i') ?: '-' }}</div>
<div class="mb-2"><strong>Terminato:</strong> {{ optional($intervento->terminato_at)->format('d/m/Y H:i') ?: '-' }}</div>
<div class="mb-2"><strong>Rif. chiusura:</strong> {{ $intervento->riferimento_chiusura ?: '-' }}</div>
<div class="mb-0"><strong>QR token:</strong> {{ $intervento->qr_token ?: '-' }}</div>
</div>
</div>
<div class="card">
<div class="card-header">Storico stesso richiedente/stabile</div>
<div class="card-body">
@forelse($storico as $old)
<div class="border-bottom pb-2 mb-2">
<div class="small fw-semibold">#{{ $old->ticket_id }} - {{ $old->ticket->titolo ?? '-' }}</div>
<div class="small text-muted">{{ $old->stato }} · {{ optional($old->updated_at)->format('d/m/Y H:i') }}</div>
@if($old->note_amministratore)
<div class="small text-muted">Nota admin: {{ \Illuminate\Support\Str::limit($old->note_amministratore, 90) }}</div>
@endif
</div>
@empty
<p class="text-muted mb-0">Nessuno storico correlato.</p>
@endforelse
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="tab-operativo">
<div class="card">
<div class="card-header">Aggiorna intervento</div>
<div class="card-header">Rapporto operativo, ricambi e apparato</div>
<div class="card-body">
@if($intervento->stato === 'assegnato')
<form method="POST" action="{{ route('fornitore.tickets.start', $intervento) }}" class="mb-3">
@ -38,22 +119,90 @@
<textarea class="form-control" name="rapporto_fornitore" id="rapporto_fornitore" rows="5" required>{{ old('rapporto_fornitore', $intervento->rapporto_fornitore) }}</textarea>
</div>
<div class="row g-2 mb-3">
<div class="col-md-4"><input type="text" class="form-control" name="apparato_marca" value="{{ old('apparato_marca') }}" placeholder="Marca apparato"></div>
<div class="col-md-4"><input type="text" class="form-control" name="apparato_modello" value="{{ old('apparato_modello') }}" placeholder="Modello"></div>
<div class="col-md-4"><input type="text" class="form-control" name="apparato_seriale" value="{{ old('apparato_seriale') }}" placeholder="Seriale"></div>
</div>
<div class="mb-3">
<label class="form-label" for="tempo_minuti">Tempo impiegato (minuti, soft)</label>
<label class="form-label" for="tempo_minuti">Tempo impiegato (minuti)</label>
<input type="number" min="1" max="1440" class="form-control" name="tempo_minuti" id="tempo_minuti" value="{{ old('tempo_minuti', $intervento->tempo_minuti) }}">
</div>
<div class="mb-3">
<label class="form-label">Articoli utilizzati</label>
<label class="form-label">Ricambi/attivita utilizzati</label>
@php($items = old('articoli_utilizzati', $intervento->articoli_utilizzati ?? []))
@for($i = 0; $i < 6; $i++)
<input type="text" class="form-control mb-2" name="articoli_utilizzati[]" value="{{ $items[$i] ?? '' }}" placeholder="Articolo/attivita {{ $i + 1 }}">
@for($i = 0; $i < 10; $i++)
<input type="text" class="form-control mb-2" name="articoli_utilizzati[]" value="{{ $items[$i] ?? '' }}" placeholder="Ricambio o attivita {{ $i + 1 }}">
@endfor
</div>
<div class="row g-2 mb-3">
<div class="col-md-6">
<label class="form-label">Foto lavoro (max 10)</label>
<input type="file" class="form-control" name="foto_lavoro[]" accept="image/*" multiple>
</div>
<div class="col-md-6">
<label class="form-label">Documenti lavoro (max 10)</label>
<input type="file" class="form-control" name="documenti_lavoro[]" accept=".pdf,.doc,.docx,.xls,.xlsx,.txt,image/*" multiple>
</div>
</div>
@for($i = 0; $i < 10; $i++)
<input type="text" class="form-control mb-2" name="descrizione_file[]" value="{{ old('descrizione_file.' . $i) }}" placeholder="Descrizione file #{{ $i + 1 }} (opzionale)">
@endfor
<div class="mb-3">
<label class="form-label" for="foto_lavoro">Foto lavoro svolto</label>
<input type="file" class="form-control" name="foto_lavoro" id="foto_lavoro" accept="image/*">
<label class="form-label" for="messaggio_veloce">Messaggio veloce allo studio (nelle note)</label>
<textarea class="form-control" name="messaggio_veloce" id="messaggio_veloce" rows="2" placeholder="Messaggio rapido">{{ old('messaggio_veloce') }}</textarea>
</div>
<button type="submit" class="btn btn-success">Salva avanzamento</button>
</form>
</div>
</div>
</div>
<div class="tab-pane fade" id="tab-comunicazioni">
<div class="card">
<div class="card-header">Note e comunicazioni ticket</div>
<div class="card-body">
@forelse($intervento->ticket->messages as $msg)
<div class="border-bottom pb-2 mb-2">
<div class="small fw-semibold">{{ $msg->user->name ?? 'Sistema' }} · {{ optional($msg->created_at)->format('d/m/Y H:i') }}</div>
<div class="small" style="white-space: pre-wrap;">{{ $msg->messaggio }}</div>
</div>
@empty
<p class="text-muted mb-0">Nessuna comunicazione disponibile.</p>
@endforelse
</div>
</div>
</div>
<div class="tab-pane fade" id="tab-chiusura">
<div class="card">
<div class="card-header">Chiusura rapportino e proforma</div>
<div class="card-body">
<form method="POST" action="{{ route('fornitore.tickets.complete', $intervento) }}" enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label class="form-label">Rapporto finale</label>
<textarea class="form-control" name="rapporto_fornitore" rows="4" required>{{ old('rapporto_fornitore', $intervento->rapporto_fornitore) }}</textarea>
</div>
<div class="row g-2 mb-3">
<div class="col-md-4"><input type="text" class="form-control" name="proforma_codice" value="{{ old('proforma_codice') }}" placeholder="Codice proforma/FE"></div>
<div class="col-md-8"><input type="text" class="form-control" name="proforma_note" value="{{ old('proforma_note') }}" placeholder="Note proforma/autorizzazione"></div>
</div>
<div class="form-check mb-2">
<input class="form-check-input" type="checkbox" name="richiesta_proforma" value="1" id="richiesta_proforma" {{ old('richiesta_proforma') ? 'checked' : '' }}>
<label class="form-check-label" for="richiesta_proforma">Richiedi autorizzazione proforma</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="lavoro_standard" value="1" id="lavoro_standard" {{ old('lavoro_standard') ? 'checked' : '' }}>
<label class="form-check-label" for="lavoro_standard">Lavoro standard gia autorizzato (passa a fatturabile)</label>
</div>
<div class="mb-3">
@ -61,38 +210,11 @@
<input type="text" class="form-control" name="qr_token" id="qr_token" placeholder="Inserisci il codice QR" value="{{ old('qr_token') }}">
</div>
<button type="submit" class="btn btn-success">Invia in verifica</button>
<button type="submit" class="btn btn-success">Chiudi rapportino e invia</button>
</form>
</div>
</div>
</div>
<div class="col-lg-5">
<div class="card mb-3">
<div class="card-header">Stato operativo</div>
<div class="card-body">
<div class="mb-2"><strong>Stato:</strong> <span class="badge bg-info text-dark">{{ $intervento->stato }}</span></div>
<div class="mb-2"><strong>Operatore:</strong> {{ $intervento->eseguitoDaDipendente?->nome_completo ?: 'Responsabile fornitore' }}</div>
<div class="mb-2"><strong>Iniziato:</strong> {{ optional($intervento->iniziato_at)->format('d/m/Y H:i') ?: '-' }}</div>
<div class="mb-2"><strong>Terminato:</strong> {{ optional($intervento->terminato_at)->format('d/m/Y H:i') ?: '-' }}</div>
<div class="mb-2"><strong>Check foto:</strong> {{ $intervento->check_foto_ok ? 'OK' : 'Da verificare' }}</div>
<div class="mb-2"><strong>Check QR:</strong> {{ $intervento->check_qr_ok ? 'OK' : 'Da verificare' }}</div>
<div class="mb-2"><strong>Check admin:</strong> {{ $intervento->check_admin_ok ? 'OK' : 'Da verificare' }}</div>
<div class="mb-2"><strong>Rif. chiusura:</strong> {{ $intervento->riferimento_chiusura ?: '-' }}</div>
<div class="mb-0"><strong>QR token:</strong> {{ $intervento->qr_token ?: '-' }}</div>
</div>
</div>
<div class="card">
<div class="card-header">Fatturazione</div>
<div class="card-body">
<div class="mb-2"><strong>Numero fattura:</strong> {{ $intervento->fattura_numero ?: '-' }}</div>
<div class="mb-2"><strong>Data:</strong> {{ optional($intervento->fattura_data)->format('d/m/Y') ?: '-' }}</div>
<div class="mb-2"><strong>Totale:</strong> {{ $intervento->fattura_totale ?: '-' }}</div>
<div class="mb-0"><strong>Rif. FE:</strong> {{ $intervento->fe_riferimento ?: '-' }}</div>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@ -18,8 +18,8 @@
use App\Http\Controllers\Admin\SoggettoController;
use App\Http\Controllers\Admin\StabileAttivoController;
use App\Http\Controllers\Admin\StabileController;
use App\Http\Controllers\Admin\TicketController;
use App\Http\Controllers\Admin\TicketAttachmentViewController;
use App\Http\Controllers\Admin\TicketController;
use App\Http\Controllers\Admin\TicketInterventoController;
use App\Http\Controllers\Admin\UnitaImmobiliareController;
use App\Http\Controllers\Admin\VoceSpesaController;
@ -629,6 +629,7 @@
Route::get('/tickets/{intervento}', [FornitoreTicketOperativoController::class, 'show'])->name('tickets.show');
Route::post('/tickets/{intervento}/start', [FornitoreTicketOperativoController::class, 'start'])->name('tickets.start');
Route::post('/tickets/{intervento}/complete', [FornitoreTicketOperativoController::class, 'complete'])->name('tickets.complete');
Route::post('/tickets/{intervento}/sollecito', [FornitoreTicketOperativoController::class, 'sollecito'])->name('tickets.sollecito');
Route::get('/dipendenti', [\App\Http\Controllers\Fornitore\DipendenteController::class, 'index'])->name('dipendenti.index');
Route::post('/dipendenti', [\App\Http\Controllers\Fornitore\DipendenteController::class, 'store'])->name('dipendenti.store');