whereHas('stabile', function ($query) { $query->where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null); }) ->orderBy('created_at', 'desc') ->paginate(10); return view('admin.tickets.index', compact('tickets')); } /** * Show the form for creating a new resource. */ public function create() { $stabili = Stabile::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null) ->attivi() ->get(); $categorieTicket = CategoriaTicket::all(); $users = User::role(['admin', 'amministratore'])->get(); $fornitori = Fornitore::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)->get(); return view('admin.tickets.create', compact('stabili', 'categorieTicket', 'users', 'fornitori')); } /** * Store a newly created resource in storage. */ public function store(Request $request) { $request->validate([ 'stabile_id' => 'required|exists:stabili,id_stabile', 'categoria_ticket_id' => 'nullable|exists:categorie_ticket,id', 'titolo' => 'required|string|max:255', 'descrizione' => 'nullable|string', 'luogo_intervento' => 'nullable|string|max:255', 'stato' => 'required|in:Aperto,Preso in Carico,In Lavorazione,In Attesa Approvazione,In Attesa Ricambi,Risolto,Chiuso,Annullato', 'priorita' => 'required|in:Bassa,Media,Alta,Urgente', 'assegnato_a_user_id' => 'nullable|exists:users,id', 'assegnato_a_fornitore_id' => 'nullable|exists:fornitori,id_fornitore', 'data_scadenza_prevista' => 'nullable|date', 'data_risoluzione_effettiva' => 'nullable|date', 'data_chiusura_effettiva' => 'nullable|date', ]); $ticket = Ticket::create([ 'stabile_id' => $request->stabile_id, 'categoria_ticket_id' => $request->categoria_ticket_id, 'aperto_da_user_id' => Auth::id(), 'assegnato_a_user_id' => $request->assegnato_a_user_id, 'assegnato_a_fornitore_id' => $request->assegnato_a_fornitore_id, 'titolo' => $request->titolo, 'descrizione' => $request->descrizione, 'luogo_intervento' => $request->luogo_intervento, 'data_apertura' => now(), 'data_scadenza_prevista' => $request->data_scadenza_prevista, 'data_risoluzione_effettiva' => $request->data_risoluzione_effettiva, 'data_chiusura_effettiva' => $request->data_chiusura_effettiva, 'stato' => $request->stato, 'priorita' => $request->priorita, ]); $this->notifySupplierAssignment($ticket); return redirect()->route('admin.tickets.index') ->with('success', 'Ticket creato con successo.'); } /** * Display the specified resource. */ public function show(Ticket $ticket) { // Verifica che l'utente possa accedere a questo ticket if ($ticket->stabile->amministratore_id !== Auth::user()->amministratore->id_amministratore ?? null) { abort(403); } $ticket->load([ 'stabile', 'categoriaTicket', 'apertoUser', 'assegnatoUser', 'assegnatoFornitore', 'messages.user', 'messages.emlDocumento', 'interventi.fornitore', 'interventi.presoInCaricoDaUser', 'insuranceClaim', ]); $comunicazioni = $ticket->messages() ->with(['user', 'emlDocumento']) ->orderByDesc('inviato_il') ->orderByDesc('created_at') ->limit(50) ->get(); $articoliPronti = [ 'Uscita tecnica ordinaria', 'Sostituzione lampada scala', 'Sostituzione pulsante/citofono', 'Riarmo quadro elettrico', 'Controllo perdita idraulica', 'Pulizia filtro pompa', ]; return view('admin.tickets.show', compact('ticket', 'comunicazioni', 'articoliPronti')); } public function storeEmailMessage(Request $request, Ticket $ticket) { if ($ticket->stabile->amministratore_id !== Auth::user()->amministratore->id_amministratore ?? null) { abort(403); } $validated = $request->validate([ 'eml_file' => 'required|file|max:20480|mimes:eml,msg,txt', 'oggetto' => 'nullable|string|max:255', 'email_mittente' => 'nullable|email|max:255', 'email_destinatario' => 'nullable|email|max:255', 'inviato_il' => 'nullable|date', 'messaggio' => 'nullable|string|max:4000', ]); $file = $request->file('eml_file'); $path = $file->store('documenti/ticket-email/' . $ticket->id, 'public'); $headers = $this->extractEmailHeadersFromEml($path); $oggetto = trim((string) ($validated['oggetto'] ?? $headers['subject'] ?? '')); $mittente = trim((string) ($validated['email_mittente'] ?? $headers['from'] ?? '')); $destinatario = trim((string) ($validated['email_destinatario'] ?? $headers['to'] ?? '')); $externalId = trim((string) ($headers['message_id'] ?? '')); $inviatoIl = $validated['inviato_il'] ?? ($headers['date'] ?? null); $documento = Documento::create([ 'documentable_id' => $ticket->id, 'documentable_type' => Ticket::class, 'stabile_id' => $ticket->stabile_id, 'utente_id' => Auth::id(), 'nome_file' => $file->getClientOriginalName(), 'path_file' => $path, 'percorso_file' => $path, 'tipo_documento' => 'Email EML', 'tipologia' => 'comunicazione', 'nome' => $oggetto !== '' ? $oggetto : 'Email associata al ticket #' . $ticket->id, 'mime_type' => (string) ($file->getMimeType() ?: 'message/rfc822'), 'estensione' => strtolower((string) $file->getClientOriginalExtension()), 'dimensione_file' => (int) ($file->getSize() ?: 0), 'data_documento' => $inviatoIl, 'data_upload' => now(), 'descrizione' => $validated['messaggio'] ?? 'Email collegata al ticket', 'tags' => 'email,ticket,comunicazioni', 'note' => 'Documento EML importato da ticket #' . $ticket->id, ]); $ticket->messages()->create([ 'user_id' => Auth::id(), 'messaggio' => trim((string) ($validated['messaggio'] ?? '')) ?: 'Email acquisita e archiviata come documento EML.', 'canale' => 'email', 'direzione' => 'inbound', 'oggetto' => $oggetto !== '' ? $oggetto : null, 'email_mittente' => $mittente !== '' ? $mittente : null, 'email_destinatario' => $destinatario !== '' ? $destinatario : null, 'external_message_id' => $externalId !== '' ? $externalId : null, 'inviato_il' => $inviatoIl, 'eml_documento_id' => $documento->id, 'metadata' => [ 'original_file_name' => $file->getClientOriginalName(), 'stored_path' => $path, ], ]); CommunicationMessage::query()->create([ 'channel' => 'email', 'direction' => 'inbound', 'external_message_id' => $externalId !== '' ? $externalId : null, 'stabile_id' => $ticket->stabile_id, 'sender_name' => $mittente !== '' ? $mittente : null, 'message_text' => trim((string) ($validated['messaggio'] ?? '')) ?: ($oggetto !== '' ? $oggetto : 'Email collegata al ticket #' . $ticket->id), 'ticket_id' => (int) $ticket->id, 'status' => 'linked_to_ticket', 'received_at' => $inviatoIl, 'metadata' => [ 'source' => 'admin_ticket_eml_import', 'email_mittente' => $mittente !== '' ? $mittente : null, 'email_destinatario' => $destinatario !== '' ? $destinatario : null, 'eml_documento_id' => (int) $documento->id, 'original_file_name' => $file->getClientOriginalName(), 'stored_path' => $path, ], ]); return redirect()->route('admin.tickets.show', $ticket) ->with('success', 'Email archiviata e collegata al ticket.'); } public function openInsuranceClaim(Request $request, Ticket $ticket) { if ($ticket->stabile->amministratore_id !== Auth::user()->amministratore->id_amministratore ?? null) { abort(403); } $validated = $request->validate([ 'policy_reference' => 'nullable|string|max:255', 'claim_number' => 'nullable|string|max:255', 'notes' => 'nullable|string|max:4000', ]); $claim = InsuranceClaim::query()->updateOrCreate( ['ticket_id' => $ticket->id], [ 'stabile_id' => $ticket->stabile_id, 'policy_reference' => $validated['policy_reference'] ?? null, 'claim_number' => $validated['claim_number'] ?? null, 'status' => 'aperta', 'opened_at' => now(), 'notes' => $validated['notes'] ?? null, ] ); $ticket->messages()->create([ 'user_id' => Auth::id(), 'messaggio' => 'Apertura sinistro assicurativo collegata al ticket. Riferimento sinistro: ' . ($claim->claim_number ?: 'da definire'), 'canale' => 'assicurazione', 'direzione' => 'outbound', 'inviato_il' => now(), 'metadata' => [ 'insurance_claim_id' => $claim->id, ], ]); return redirect()->route('admin.tickets.show', $ticket) ->with('success', 'Sinistro assicurativo collegato al ticket.'); } /** * Estrae gli header principali da un file EML senza dipendenze esterne. * * @return array{subject:?string,from:?string,to:?string,date:?string,message_id:?string} */ private function extractEmailHeadersFromEml(string $storedPath): array { $fullPath = Storage::disk('public')->path($storedPath); if (! is_readable($fullPath)) { return [ 'subject' => null, 'from' => null, 'to' => null, 'date' => null, 'message_id' => null, ]; } $content = (string) @file_get_contents($fullPath); if ($content === '') { return [ 'subject' => null, 'from' => null, 'to' => null, 'date' => null, 'message_id' => null, ]; } $headerBlock = strstr($content, "\n\n", true); $headerBlock = $headerBlock === false ? substr($content, 0, 8000) : $headerBlock; $extract = function (string $name) use ($headerBlock): ?string { $pattern = '/^' . preg_quote($name, '/') . ':(.*)$/im'; if (! preg_match($pattern, $headerBlock, $matches)) { return null; } return trim((string) $matches[1]); }; return [ 'subject' => $extract('Subject'), 'from' => $extract('From'), 'to' => $extract('To'), 'date' => $extract('Date'), 'message_id' => $extract('Message-ID'), ]; } /** * Show the form for editing the specified resource. */ public function edit(Ticket $ticket) { // Verifica che l'utente possa modificare questo ticket if ($ticket->stabile->amministratore_id !== Auth::user()->amministratore->id_amministratore ?? null) { abort(403); } $stabili = Stabile::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null) ->attivi() ->get(); $categorieTicket = CategoriaTicket::all(); $users = User::role(['admin', 'amministratore'])->get(); $fornitori = Fornitore::where('amministratore_id', Auth::user()->amministratore->id_amministratore ?? null)->get(); return view('admin.tickets.edit', compact('ticket', 'stabili', 'categorieTicket', 'users', 'fornitori')); } /** * Update the specified resource in storage. */ public function update(Request $request, Ticket $ticket) { // Verifica che l'utente possa modificare questo ticket if ($ticket->stabile->amministratore_id !== Auth::user()->amministratore->id_amministratore ?? null) { abort(403); } $request->validate([ 'stabile_id' => 'required|exists:stabili,id_stabile', 'categoria_ticket_id' => 'nullable|exists:categorie_ticket,id', 'titolo' => 'required|string|max:255', 'descrizione' => 'nullable|string', 'luogo_intervento' => 'nullable|string|max:255', 'stato' => 'required|in:Aperto,Preso in Carico,In Lavorazione,In Attesa Approvazione,In Attesa Ricambi,Risolto,Chiuso,Annullato', 'priorita' => 'required|in:Bassa,Media,Alta,Urgente', 'assegnato_a_user_id' => 'nullable|exists:users,id', 'assegnato_a_fornitore_id' => 'nullable|exists:fornitori,id_fornitore', 'data_scadenza_prevista' => 'nullable|date', 'data_risoluzione_effettiva' => 'nullable|date', 'data_chiusura_effettiva' => 'nullable|date', ]); $previousSupplierId = $ticket->assegnato_a_fornitore_id; $ticket->update($request->all()); if ((int) ($ticket->assegnato_a_fornitore_id ?? 0) > 0 && (int) $previousSupplierId !== (int) $ticket->assegnato_a_fornitore_id) { $this->notifySupplierAssignment($ticket); } return redirect()->route('admin.tickets.index') ->with('success', 'Ticket aggiornato con successo.'); } private function notifySupplierAssignment(Ticket $ticket): void { $ticket->loadMissing(['assegnatoAFornitore', 'stabile']); $fornitore = $ticket->assegnatoAFornitore; if (! $fornitore || empty($fornitore->email)) { return; } $subject = sprintf('NetGescon Ticket #%d assegnato', (int) $ticket->id); $body = implode(PHP_EOL, [ 'Nuovo ticket assegnato', '', 'ID ticket: ' . (int) $ticket->id, 'Titolo: ' . (string) $ticket->titolo, 'Stabile: ' . (string) optional($ticket->stabile)->denominazione, 'Priorita: ' . (string) $ticket->priorita, 'Stato: ' . (string) $ticket->stato, 'Luogo intervento: ' . (string) ($ticket->luogo_intervento ?? '-'), '', 'Descrizione:', (string) ($ticket->descrizione ?? '-'), ]); try { Mail::raw($body, function ($message) use ($fornitore, $subject): void { $message->to($fornitore->email)->subject($subject); }); } catch (\Throwable $e) { Log::warning('Ticket supplier notification failed', [ 'ticket_id' => $ticket->id, 'fornitore_id' => $fornitore->id, 'error' => $e->getMessage(), ]); } } /** * Remove the specified resource from storage. */ public function destroy(Ticket $ticket) { // Verifica che l'utente possa eliminare questo ticket if ($ticket->stabile->amministratore_id !== Auth::user()->amministratore->id_amministratore ?? null) { abort(403); } $ticket->delete(); return redirect()->route('admin.tickets.index') ->with('success', 'Ticket eliminato con successo.'); } public function close(Ticket $ticket) { if ($ticket->stabile->amministratore_id !== Auth::user()->amministratore->id_amministratore ?? null) { abort(403); } if ($ticket->stato === 'Chiuso') { return redirect()->route('admin.tickets.show', $ticket) ->with('success', 'Il ticket era gia chiuso.'); } $ticket->update([ 'stato' => 'Chiuso', 'data_risoluzione_effettiva' => $ticket->data_risoluzione_effettiva ?? now(), 'data_chiusura_effettiva' => now(), ]); return redirect()->route('admin.tickets.show', $ticket) ->with('success', 'Ticket chiuso con successo.'); } }