612 lines
21 KiB
PHP
612 lines
21 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Supporto;
|
|
|
|
use App\Models\Fornitore;
|
|
use App\Models\Ticket;
|
|
use App\Models\TicketAttachment;
|
|
use App\Models\TicketIntervento;
|
|
use App\Models\User;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\WithFileUploads;
|
|
use UnitEnum;
|
|
|
|
class TicketGestione extends Page
|
|
{
|
|
use WithFileUploads;
|
|
|
|
protected static ?string $navigationLabel = 'Ticket Gestione';
|
|
|
|
protected static ?string $title = 'Gestione Ticket';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Supporto';
|
|
|
|
protected static ?int $navigationSort = 26;
|
|
|
|
protected static ?string $slug = 'supporto/ticket-gestione';
|
|
|
|
protected string $view = 'filament.pages.supporto.ticket-gestione';
|
|
|
|
public string $status = 'open';
|
|
|
|
public string $activeTab = 'elenco';
|
|
|
|
public ?int $selectedTicketId = null;
|
|
|
|
public ?string $notaInterna = null;
|
|
|
|
public ?int $fornitoreId = null;
|
|
|
|
public ?string $noteAssegnazione = null;
|
|
|
|
/** @var array<int,mixed> */
|
|
public array $nuoviAllegati = [];
|
|
|
|
/** @var array<int,mixed> */
|
|
public array $nuoveFoto = [];
|
|
|
|
public ?string $descrizioneAllegati = null;
|
|
|
|
/** @var array<int,array{id:int,nome:string}> */
|
|
public array $fornitoriOptions = [];
|
|
|
|
/** @var array<int,array<string,mixed>> */
|
|
public array $fornitoriAttiviRows = [];
|
|
|
|
/** @var array<string,mixed>|null */
|
|
public ?array $attachmentPreview = null;
|
|
|
|
/** @var \Illuminate\Support\Collection<int, Ticket> */
|
|
public $tickets;
|
|
|
|
/** @var array<string,int> */
|
|
public array $ticketCounters = [
|
|
'open' => 0,
|
|
'urgent' => 0,
|
|
'closed' => 0,
|
|
'all' => 0,
|
|
];
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->tickets = collect();
|
|
|
|
$this->selectedTicketId = (int) request()->query('ticket', 0) ?: null;
|
|
$queryTab = (string) request()->query('tab', '');
|
|
if ($queryTab === 'scheda') {
|
|
$this->activeTab = 'scheda';
|
|
}
|
|
|
|
if (request()->query('status') === 'all') {
|
|
$this->status = 'all';
|
|
}
|
|
|
|
$this->loadFornitoriOptions();
|
|
$this->refreshFornitoriAttiviRows();
|
|
if ($this->selectedTicket) {
|
|
$this->fornitoreId = (int) ($this->selectedTicket->assegnato_a_fornitore_id ?? 0) ?: null;
|
|
}
|
|
|
|
$this->refreshData();
|
|
}
|
|
|
|
public function updatedStatus(): void
|
|
{
|
|
$this->refreshData();
|
|
}
|
|
|
|
public function refreshData(): void
|
|
{
|
|
$this->loadCounters();
|
|
$this->loadTickets();
|
|
$this->refreshFornitoriAttiviRows();
|
|
}
|
|
|
|
public function getTicketInserimentoUrl(): string
|
|
{
|
|
return TicketMobile::getUrl(panel: 'admin-filament');
|
|
}
|
|
|
|
public function apriScheda(int $ticketId): void
|
|
{
|
|
$this->selectedTicketId = $ticketId;
|
|
$this->activeTab = 'scheda';
|
|
|
|
$ticket = $this->selectedTicket;
|
|
$this->fornitoreId = $ticket ? ((int) ($ticket->assegnato_a_fornitore_id ?? 0) ?: null): null;
|
|
}
|
|
|
|
public function apriElenco(): void
|
|
{
|
|
$this->activeTab = 'elenco';
|
|
}
|
|
|
|
public function getFornitoreOperativoUrl(?int $fornitoreId = null): string
|
|
{
|
|
$base = route('fornitore.tickets.index');
|
|
if ($fornitoreId) {
|
|
return $base . '?fornitore=' . $fornitoreId;
|
|
}
|
|
|
|
return $base;
|
|
}
|
|
|
|
public function getFornitoreAnagraficaUrl(int $fornitoreId): string
|
|
{
|
|
return \App\Filament\Pages\Gescon\FornitoreScheda::getUrl(['record' => $fornitoreId], panel: 'admin-filament');
|
|
}
|
|
|
|
public function getAttachmentPublicUrl(TicketAttachment $attachment): string
|
|
{
|
|
return route('admin.tickets.attachments.view', ['attachment' => (int) $attachment->id]);
|
|
}
|
|
|
|
public function openAttachmentPreview(int $attachmentId): void
|
|
{
|
|
$ticket = $this->selectedTicket;
|
|
if (! $ticket) {
|
|
return;
|
|
}
|
|
|
|
$attachment = $ticket->attachments->firstWhere('id', $attachmentId);
|
|
if (! $attachment instanceof TicketAttachment) {
|
|
return;
|
|
}
|
|
|
|
$name = (string) ($attachment->original_file_name ?? 'allegato');
|
|
$mime = strtolower((string) ($attachment->mime_type ?? ''));
|
|
$ext = strtolower((string) pathinfo($name, PATHINFO_EXTENSION));
|
|
|
|
$isImage = str_starts_with($mime, 'image/') || in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp'], true);
|
|
$isPdf = $mime === 'application/pdf' || $ext === 'pdf';
|
|
|
|
$this->attachmentPreview = [
|
|
'id' => (int) $attachment->id,
|
|
'name' => $name,
|
|
'description' => (string) ($attachment->description ?? ''),
|
|
'url' => $this->getAttachmentPublicUrl($attachment),
|
|
'is_image' => $isImage,
|
|
'is_pdf' => $isPdf,
|
|
];
|
|
}
|
|
|
|
public function closeAttachmentPreview(): void
|
|
{
|
|
$this->attachmentPreview = null;
|
|
}
|
|
|
|
public function getSelectedTicketProperty(): ?Ticket
|
|
{
|
|
if (! $this->selectedTicketId) {
|
|
return null;
|
|
}
|
|
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return null;
|
|
}
|
|
|
|
return Ticket::query()
|
|
->with([
|
|
'stabile:id,denominazione',
|
|
'categoriaTicket:id,nome',
|
|
'assegnatoAFornitore:id,ragione_sociale,nome,cognome',
|
|
'attachments.user:id,name',
|
|
'messages.user:id,name',
|
|
'interventi.fornitore:id,ragione_sociale,nome,cognome',
|
|
'interventi.creatoDaUser:id,name',
|
|
])
|
|
->where('stabile_id', $stabileId)
|
|
->find($this->selectedTicketId);
|
|
}
|
|
|
|
public function assegnaFornitore(): void
|
|
{
|
|
$ticket = $this->selectedTicket;
|
|
if (! $ticket) {
|
|
Notification::make()->title('Ticket non trovato')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'fornitoreId' => ['required', 'integer', 'exists:fornitori,id'],
|
|
'noteAssegnazione' => ['nullable', 'string', 'max:4000'],
|
|
]);
|
|
|
|
$ticket->update([
|
|
'assegnato_a_fornitore_id' => $this->fornitoreId,
|
|
'stato' => in_array($ticket->stato, ['Aperto'], true) ? 'Preso in Carico' : $ticket->stato,
|
|
]);
|
|
|
|
if (Schema::hasTable('ticket_interventi')) {
|
|
$ultimo = TicketIntervento::query()
|
|
->where('ticket_id', $ticket->id)
|
|
->where('fornitore_id', $this->fornitoreId)
|
|
->latest('id')
|
|
->first();
|
|
|
|
if (! $ultimo) {
|
|
TicketIntervento::query()->create([
|
|
'ticket_id' => $ticket->id,
|
|
'fornitore_id' => $this->fornitoreId,
|
|
'creato_da_user_id' => Auth::id(),
|
|
'stato' => 'assegnato',
|
|
'note_amministratore' => $this->noteAssegnazione,
|
|
]);
|
|
}
|
|
}
|
|
|
|
if (filled($this->noteAssegnazione)) {
|
|
$ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => 'Assegnazione fornitore: ' . trim((string) $this->noteAssegnazione),
|
|
]);
|
|
}
|
|
|
|
$this->noteAssegnazione = null;
|
|
|
|
Notification::make()->title('Fornitore assegnato')->success()->send();
|
|
$this->refreshData();
|
|
}
|
|
|
|
public function aggiungiNotaInterna(): void
|
|
{
|
|
$ticket = $this->selectedTicket;
|
|
if (! $ticket) {
|
|
Notification::make()->title('Ticket non trovato')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'notaInterna' => ['required', 'string', 'max:5000'],
|
|
]);
|
|
|
|
$stamp = now()->format('d/m/Y H:i');
|
|
$ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => '[' . $stamp . '] ' . trim((string) $this->notaInterna),
|
|
]);
|
|
|
|
$this->notaInterna = null;
|
|
|
|
Notification::make()->title('Nota aggiunta')->success()->send();
|
|
}
|
|
|
|
public function caricaAllegati(): void
|
|
{
|
|
$ticket = $this->selectedTicket;
|
|
if (! $ticket) {
|
|
Notification::make()->title('Ticket non trovato')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
if (! Schema::hasTable('ticket_attachments')) {
|
|
Notification::make()->title('Tabella allegati non pronta')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'nuoviAllegati' => ['nullable', 'array', 'max:8'],
|
|
'nuoviAllegati.*' => ['file', 'max:10240', 'mimes:jpg,jpeg,png,webp,pdf,doc,docx,xls,xlsx,txt'],
|
|
'nuoveFoto' => ['nullable', 'array', 'max:8'],
|
|
'nuoveFoto.*' => ['file', 'max:10240', 'mimes:jpg,jpeg,png,webp'],
|
|
'descrizioneAllegati' => ['nullable', 'string', 'max:500'],
|
|
]);
|
|
|
|
$files = array_merge($this->nuoveFoto, $this->nuoviAllegati);
|
|
if (count($files) === 0) {
|
|
Notification::make()->title('Seleziona almeno un file o una foto')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$saved = 0;
|
|
foreach ($files as $file) {
|
|
if (! is_object($file) || ! method_exists($file, 'store')) {
|
|
continue;
|
|
}
|
|
|
|
$path = $file->store('ticket-gestione/' . $ticket->id, 'public');
|
|
|
|
TicketAttachment::query()->create([
|
|
'ticket_id' => $ticket->id,
|
|
'ticket_update_id' => null,
|
|
'user_id' => Auth::id(),
|
|
'file_path' => $path,
|
|
'original_file_name' => (string) $file->getClientOriginalName(),
|
|
'mime_type' => (string) $file->getClientMimeType(),
|
|
'size' => (int) $file->getSize(),
|
|
'description' => $this->descrizioneAllegati ?: 'Allegato da gestione ticket',
|
|
]);
|
|
|
|
$saved++;
|
|
}
|
|
|
|
$this->nuoveFoto = [];
|
|
$this->nuoviAllegati = [];
|
|
$this->descrizioneAllegati = null;
|
|
|
|
Notification::make()
|
|
->title('Allegati caricati')
|
|
->body('Caricati ' . $saved . ' allegati.')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
public function prendiInCarico(int $ticketId): void
|
|
{
|
|
$this->aggiornaStatoTicket($ticketId, 'Preso in Carico', true);
|
|
}
|
|
|
|
public function avviaLavorazione(int $ticketId): void
|
|
{
|
|
$this->aggiornaStatoTicket($ticketId, 'In Lavorazione');
|
|
}
|
|
|
|
public function risolviTicket(int $ticketId): void
|
|
{
|
|
$this->aggiornaStatoTicket($ticketId, 'Risolto');
|
|
}
|
|
|
|
public function chiudiTicket(int $ticketId): void
|
|
{
|
|
$this->aggiornaStatoTicket($ticketId, 'Chiuso');
|
|
}
|
|
|
|
private function loadTickets(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
$this->tickets = collect();
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
$this->tickets = collect();
|
|
return;
|
|
}
|
|
|
|
$query = Ticket::query()
|
|
->with(['categoriaTicket', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome'])
|
|
->where('stabile_id', $stabileId);
|
|
|
|
if ($this->status === 'open') {
|
|
$query->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']);
|
|
} elseif ($this->status === 'urgent') {
|
|
$query->where('priorita', 'Urgente')
|
|
->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione']);
|
|
} elseif ($this->status === 'closed') {
|
|
$query->whereIn('stato', ['Risolto', 'Chiuso']);
|
|
}
|
|
|
|
$this->tickets = $query->latest('data_apertura')->limit(50)->get();
|
|
}
|
|
|
|
private function refreshFornitoriAttiviRows(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
$this->fornitoriAttiviRows = [];
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
$this->fornitoriAttiviRows = [];
|
|
return;
|
|
}
|
|
|
|
$ticketApertiStati = ['Aperto', 'Preso in Carico', 'In Lavorazione', 'In Attesa Approvazione', 'In Attesa Ricambi'];
|
|
|
|
$assignedTickets = Ticket::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereIn('stato', $ticketApertiStati)
|
|
->whereNotNull('assegnato_a_fornitore_id')
|
|
->get(['id', 'assegnato_a_fornitore_id', 'stato', 'updated_at']);
|
|
|
|
$assignedByFornitore = $assignedTickets
|
|
->groupBy('assegnato_a_fornitore_id')
|
|
->map(fn($rows) => $rows->count());
|
|
|
|
$ticketSummaries = $assignedTickets
|
|
->groupBy('assegnato_a_fornitore_id')
|
|
->map(function ($rows) {
|
|
return $rows->sortByDesc('updated_at')->take(6)->map(function ($ticket): string {
|
|
return '#' . ((int) $ticket->id) . ' (' . (string) $ticket->stato . ')';
|
|
})->values()->all();
|
|
});
|
|
|
|
$openInterventiByFornitore = collect();
|
|
$interventoStatiByFornitore = collect();
|
|
$lastInterventoAtByFornitore = collect();
|
|
if (Schema::hasTable('ticket_interventi')) {
|
|
$openInterventi = TicketIntervento::query()
|
|
->whereIn('stato', ['assegnato', 'in_corso', 'in_verifica'])
|
|
->whereHas('ticket', function ($q) use ($stabileId, $ticketApertiStati): void {
|
|
$q->where('stabile_id', $stabileId)->whereIn('stato', $ticketApertiStati);
|
|
})
|
|
->orderByDesc('updated_at')
|
|
->get(['fornitore_id', 'stato', 'updated_at']);
|
|
|
|
$openInterventiByFornitore = $openInterventi
|
|
->groupBy('fornitore_id')
|
|
->map(fn($rows) => $rows->count());
|
|
|
|
$interventoStatiByFornitore = $openInterventi
|
|
->groupBy('fornitore_id')
|
|
->map(function ($rows): string {
|
|
return $rows->pluck('stato')
|
|
->map(fn($stato) => str_replace('_', ' ', (string) $stato))
|
|
->unique()
|
|
->take(3)
|
|
->implode(', ');
|
|
});
|
|
|
|
$lastInterventoAtByFornitore = $openInterventi
|
|
->groupBy('fornitore_id')
|
|
->map(function ($rows): ?string {
|
|
$last = $rows->sortByDesc('updated_at')->first();
|
|
return $last && $last->updated_at ? $last->updated_at->format('d/m/Y H:i') : null;
|
|
});
|
|
}
|
|
|
|
$fornitoriIds = collect($assignedByFornitore->keys())
|
|
->merge($openInterventiByFornitore->keys())
|
|
->map(fn($v) => (int) $v)
|
|
->filter(fn(int $v) => $v > 0)
|
|
->unique()
|
|
->values();
|
|
|
|
if ($fornitoriIds->isEmpty()) {
|
|
$this->fornitoriAttiviRows = [];
|
|
return;
|
|
}
|
|
|
|
$rows = [];
|
|
foreach (Fornitore::query()->whereIn('id', $fornitoriIds->all())->orderBy('ragione_sociale')->orderBy('cognome')->orderBy('nome')->get() as $fornitore) {
|
|
$rows[] = [
|
|
'id' => (int) $fornitore->id,
|
|
'nome' => trim((string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? '')))),
|
|
'email' => (string) ($fornitore->email ?? ''),
|
|
'telefono' => (string) ($fornitore->telefono ?: $fornitore->cellulare),
|
|
'ticket_attivi' => (int) ($assignedByFornitore->get((string) $fornitore->id) ?? $assignedByFornitore->get((int) $fornitore->id) ?? 0),
|
|
'interventi_aperti' => (int) ($openInterventiByFornitore->get((string) $fornitore->id) ?? $openInterventiByFornitore->get((int) $fornitore->id) ?? 0),
|
|
'ticket_focus' => (array) ($ticketSummaries->get((string) $fornitore->id) ?? $ticketSummaries->get((int) $fornitore->id) ?? []),
|
|
'intervento_stati' => (string) ($interventoStatiByFornitore->get((string) $fornitore->id) ?? $interventoStatiByFornitore->get((int) $fornitore->id) ?? ''),
|
|
'ultimo_aggiornamento' => (string) ($lastInterventoAtByFornitore->get((string) $fornitore->id) ?? $lastInterventoAtByFornitore->get((int) $fornitore->id) ?? ''),
|
|
];
|
|
}
|
|
|
|
usort($rows, function (array $a, array $b): int {
|
|
$lhs = ((int) $a['interventi_aperti']) * 1000 + (int) $a['ticket_attivi'];
|
|
$rhs = ((int) $b['interventi_aperti']) * 1000 + (int) $b['ticket_attivi'];
|
|
return $rhs <=> $lhs;
|
|
});
|
|
|
|
$this->fornitoriAttiviRows = $rows;
|
|
}
|
|
|
|
private function loadFornitoriOptions(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
$this->fornitoriOptions = [];
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
$this->fornitoriOptions = [];
|
|
return;
|
|
}
|
|
|
|
$stabile = \App\Models\Stabile::query()->find($stabileId);
|
|
$adminId = (int) ($stabile->amministratore_id ?? 0);
|
|
|
|
$query = Fornitore::query()->orderBy('ragione_sociale')->orderBy('cognome')->orderBy('nome');
|
|
if ($adminId > 0) {
|
|
$query->where(function ($q) use ($adminId): void {
|
|
$q->where('amministratore_id', $adminId)->orWhereNull('amministratore_id');
|
|
});
|
|
}
|
|
|
|
$this->fornitoriOptions = $query->limit(400)->get()->map(function (Fornitore $f): array {
|
|
$label = trim((string) ($f->ragione_sociale ?: trim(($f->nome ?? '') . ' ' . ($f->cognome ?? ''))));
|
|
return [
|
|
'id' => (int) $f->id,
|
|
'nome' => $label !== '' ? $label : ('Fornitore #' . $f->id),
|
|
];
|
|
})->all();
|
|
}
|
|
|
|
private function loadCounters(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
$this->ticketCounters = ['open' => 0, 'urgent' => 0, 'closed' => 0, 'all' => 0];
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
$this->ticketCounters = ['open' => 0, 'urgent' => 0, 'closed' => 0, 'all' => 0];
|
|
return;
|
|
}
|
|
|
|
$base = Ticket::query()->where('stabile_id', $stabileId);
|
|
|
|
$this->ticketCounters = [
|
|
'open' => (clone $base)->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
|
|
'urgent' => (clone $base)->where('priorita', 'Urgente')->whereIn('stato', ['Aperto', 'Preso in Carico', 'In Lavorazione'])->count(),
|
|
'closed' => (clone $base)->whereIn('stato', ['Risolto', 'Chiuso'])->count(),
|
|
'all' => (clone $base)->count(),
|
|
];
|
|
}
|
|
|
|
private function aggiornaStatoTicket(int $ticketId, string $nuovoStato, bool $assegnaAUtente = false): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return;
|
|
}
|
|
|
|
$ticket = Ticket::query()
|
|
->where('id', $ticketId)
|
|
->where('stabile_id', $stabileId)
|
|
->first();
|
|
|
|
if (! $ticket) {
|
|
Notification::make()
|
|
->title('Ticket non trovato o non accessibile')
|
|
->danger()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$payload = ['stato' => $nuovoStato];
|
|
if ($assegnaAUtente) {
|
|
$payload['assegnato_a_user_id'] = $user->id;
|
|
}
|
|
if ($nuovoStato === 'Risolto' && ! $ticket->data_risoluzione_effettiva) {
|
|
$payload['data_risoluzione_effettiva'] = now()->toDateString();
|
|
}
|
|
if ($nuovoStato === 'Chiuso' && ! $ticket->data_chiusura_effettiva) {
|
|
$payload['data_chiusura_effettiva'] = now()->toDateString();
|
|
}
|
|
|
|
$ticket->update($payload);
|
|
|
|
Notification::make()
|
|
->title('Ticket #' . $ticket->id . ' aggiornato a ' . $nuovoStato)
|
|
->success()
|
|
->send();
|
|
|
|
$this->refreshData();
|
|
}
|
|
}
|