netgescon-day0/app/Http/Controllers/Admin/TicketController.php

424 lines
17 KiB
PHP
Executable File

<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\CategoriaTicket;
use App\Models\Documento;
use App\Models\Fornitore;
use App\Models\InsuranceClaim;
use App\Models\Stabile;
use App\Models\Ticket;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
class TicketController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index()
{
$tickets = Ticket::with(['stabile', 'categoriaTicket', 'apertoUser', 'assegnatoUser', 'assegnatoFornitore'])
->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,
],
]);
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.');
}
}