resolveOperatoreContext(); $stato = (string) $request->query('stato', 'aperti'); $query = TicketIntervento::query() ->with(['ticket.stabile']) ->where('fornitore_id', $fornitore->id) ->orderByDesc('created_at'); if ($stato === 'chiusi') { $query->whereIn('stato', ['chiuso', 'fatturato']); } elseif ($stato === 'fatturabili') { $query->whereIn('stato', ['fatturabile', 'fatturato']); } else { $query->whereNotIn('stato', ['chiuso']); } $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']) ->orderByDesc('updated_at') ->limit(100) ->get(); return view('fornitore.tickets.index', [ 'fornitore' => $fornitore, 'interventi' => $interventi, 'rows' => $rows, 'stato' => $stato, 'fatturabili' => $fatturabili, ]); } public function show(TicketIntervento $intervento) { [$fornitore] = $this->resolveOperatoreContext(); $this->authorizeIntervento($intervento, $fornitore); $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, ]); } public function start(TicketIntervento $intervento) { [$fornitore, $dipendente] = $this->resolveOperatoreContext(); $this->authorizeIntervento($intervento, $fornitore); $intervento->update([ 'stato' => 'in_corso', 'preso_in_carico_da_user_id' => Auth::id(), 'eseguito_da_dipendente_id' => $dipendente?->id, 'iniziato_at' => $intervento->iniziato_at ?? now(), ]); $intervento->ticket->update(['stato' => 'In Lavorazione']); return redirect()->route('fornitore.tickets.show', $intervento) ->with('success', 'Intervento preso in carico.'); } public function complete(Request $request, TicketIntervento $intervento) { [$fornitore, $dipendente] = $this->resolveOperatoreContext(); $this->authorizeIntervento($intervento, $fornitore); $validated = $request->validate([ 'rapporto_fornitore' => 'required|string|max:5000', 'tempo_minuti' => 'nullable|integer|min:1|max:1440', 'articoli_utilizzati' => 'nullable|array', 'articoli_utilizzati.*' => 'nullable|string|max:120', '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' => $rapporto, 'tempo_minuti' => $validated['tempo_minuti'] ?? null, 'terminato_at' => now(), 'stato' => ! empty($validated['lavoro_standard']) ? 'fatturabile' : 'in_verifica', 'eseguito_da_dipendente_id' => $dipendente?->id ?? $intervento->eseguito_da_dipendente_id, ]; $articoli = collect($validated['articoli_utilizzati'] ?? []) ->map(fn($item) => trim((string) $item)) ->filter() ->values() ->all(); if ($articoli !== []) { $payload['articoli_utilizzati'] = $articoli; } $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'] ?? '')); if ($tokenInserito !== '' && strcasecmp($tokenInserito, (string) $intervento->qr_token) === 0) { $payload['qr_scansionato_at'] = now(); } $intervento->update($payload); $stamp = now()->format('d/m/Y H:i'); $intervento->ticket->messages()->create([ 'user_id' => Auth::id(), '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} */ private function resolveOperatoreContext(): array { $user = Auth::user(); if ($user && $user->hasAnyRole(['super-admin', 'admin', 'amministratore'])) { $fornitoreId = (int) request()->query('fornitore', 0); if ($fornitoreId <= 0) { $interventoRoute = request()->route('intervento'); if ($interventoRoute instanceof TicketIntervento) { $fornitoreId = (int) ($interventoRoute->fornitore_id ?? 0); } } if ($fornitoreId > 0) { $fornitoreAdmin = Fornitore::query()->find($fornitoreId); abort_unless($fornitoreAdmin, 404); return [$fornitoreAdmin, null]; } } $dipendente = null; $fornitore = Fornitore::query() ->where('email', (string) $user->email) ->first(); if (! $fornitore && $user) { $dipendente = FornitoreDipendente::query() ->where('attivo', true) ->where(function ($q) use ($user): void { $q->where('user_id', (int) $user->id) ->orWhereRaw('LOWER(email) = ?', [mb_strtolower((string) $user->email)]); }) ->orderByDesc('id') ->first(); if ($dipendente) { $fornitore = Fornitore::query()->find((int) $dipendente->fornitore_id); $dipendente->ultimo_accesso_at = now(); $dipendente->save(); } } abort_unless($fornitore, 403, 'Profilo fornitore non collegato.'); return [$fornitore, $dipendente]; } private function authorizeIntervento(TicketIntervento $intervento, Fornitore $fornitore): void { 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) : ''); } }