From 84215abe73a17b01da2b7e6ee422bf0efbd3440d Mon Sep 17 00:00:00 2001 From: michele Date: Thu, 12 Mar 2026 16:55:52 +0000 Subject: [PATCH] feat(fornitore): tabbed operational workspace and quick reminders --- .../Fornitore/TicketOperativoController.php | 220 +++++++++++++++++- docs/NETGESCON-MODIFICHE.md | 2 + .../pages/supporto/ticket-gestione.blade.php | 5 +- .../views/fornitore/tickets/index.blade.php | 38 ++- .../views/fornitore/tickets/show.blade.php | 204 ++++++++++++---- routes/web.php | 3 +- 6 files changed, 413 insertions(+), 59 deletions(-) diff --git a/app/Http/Controllers/Fornitore/TicketOperativoController.php b/app/Http/Controllers/Fornitore/TicketOperativoController.php index 31c361b..d6f5993 100644 --- a/app/Http/Controllers/Fornitore/TicketOperativoController.php +++ b/app/Http/Controllers/Fornitore/TicketOperativoController.php @@ -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) : ''); + } } diff --git a/docs/NETGESCON-MODIFICHE.md b/docs/NETGESCON-MODIFICHE.md index a209f4b..0d03783 100644 --- a/docs/NETGESCON-MODIFICHE.md +++ b/docs/NETGESCON-MODIFICHE.md @@ -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] | diff --git a/resources/views/filament/pages/supporto/ticket-gestione.blade.php b/resources/views/filament/pages/supporto/ticket-gestione.blade.php index 19d7012..c73857f 100644 --- a/resources/views/filament/pages/supporto/ticket-gestione.blade.php +++ b/resources/views/filament/pages/supporto/ticket-gestione.blade.php @@ -21,6 +21,9 @@ + @if($activeTab === 'elenco') +
+
+
+
+
+
- +
- + @php($items = old('articoli_utilizzati', $intervento->articoli_utilizzati ?? [])) - @for($i = 0; $i < 6; $i++) - + @for($i = 0; $i < 10; $i++) + @endfor
+
+
+ + +
+
+ + +
+
+ + @for($i = 0; $i < 10; $i++) + + @endfor +
- - + + +
+ + + + + + + +
+
+
Note e comunicazioni ticket
+
+ @forelse($intervento->ticket->messages as $msg) +
+
{{ $msg->user->name ?? 'Sistema' }} · {{ optional($msg->created_at)->format('d/m/Y H:i') }}
+
{{ $msg->messaggio }}
+
+ @empty +

Nessuna comunicazione disponibile.

+ @endforelse +
+
+
+ +
+
+
Chiusura rapportino e proforma
+
+
+ @csrf +
+ + +
+ +
+
+
+
+ +
+ + +
+
+ +
@@ -61,38 +210,11 @@
- +
- -
-
-
Stato operativo
-
-
Stato: {{ $intervento->stato }}
-
Operatore: {{ $intervento->eseguitoDaDipendente?->nome_completo ?: 'Responsabile fornitore' }}
-
Iniziato: {{ optional($intervento->iniziato_at)->format('d/m/Y H:i') ?: '-' }}
-
Terminato: {{ optional($intervento->terminato_at)->format('d/m/Y H:i') ?: '-' }}
-
Check foto: {{ $intervento->check_foto_ok ? 'OK' : 'Da verificare' }}
-
Check QR: {{ $intervento->check_qr_ok ? 'OK' : 'Da verificare' }}
-
Check admin: {{ $intervento->check_admin_ok ? 'OK' : 'Da verificare' }}
-
Rif. chiusura: {{ $intervento->riferimento_chiusura ?: '-' }}
-
QR token: {{ $intervento->qr_token ?: '-' }}
-
-
- -
-
Fatturazione
-
-
Numero fattura: {{ $intervento->fattura_numero ?: '-' }}
-
Data: {{ optional($intervento->fattura_data)->format('d/m/Y') ?: '-' }}
-
Totale: {{ $intervento->fattura_totale ?: '-' }}
-
Rif. FE: {{ $intervento->fe_riferimento ?: '-' }}
-
-
-
@endsection diff --git a/routes/web.php b/routes/web.php index 16319c0..a8b3d51 100755 --- a/routes/web.php +++ b/routes/web.php @@ -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');