netgescon-day0/app/Filament/Pages/Fornitore/TicketInterventoScheda.php

560 lines
21 KiB
PHP

<?php
namespace App\Filament\Pages\Fornitore;
use App\Filament\Pages\Fornitore\Concerns\ResolvesOperatoreContext;
use App\Models\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\Ticket;
use App\Models\TicketAttachment;
use App\Models\TicketIntervento;
use App\Models\User;
use BackedEnum;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
use Livewire\WithFileUploads;
use UnitEnum;
class TicketInterventoScheda extends Page
{
use ResolvesOperatoreContext;
use WithFileUploads;
protected static ?string $title = 'Scheda intervento';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static UnitEnum|string|null $navigationGroup = 'Fornitore';
protected static ?string $slug = 'fornitore/tickets/{record}';
protected static bool $shouldRegisterNavigation = false;
protected string $view = 'filament.pages.fornitore.ticket-intervento-scheda';
public TicketIntervento $intervento;
public Fornitore $fornitore;
public ?FornitoreDipendente $dipendente = null;
/** @var array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string} */
public array $caller = [
'contatto' => '-',
'telefono' => '',
'email' => '',
'problema' => '',
'sorgente' => '',
'riferimento' => '',
];
/** @var array<int, array<string, mixed>> */
public array $storicoRows = [];
/** @var array<int, array<string, mixed>> */
public array $dipendentiOptions = [];
public ?int $dipendenteId = null;
public string $rapportoFornitore = '';
public ?int $tempoMinuti = null;
/** @var array<int, string> */
public array $articoliUtilizzati = ['', '', ''];
public string $apparatoMarca = '';
public string $apparatoModello = '';
public string $apparatoSeriale = '';
/** @var array<int, mixed> */
public array $fotoLavoro = [];
/** @var array<int, mixed> */
public array $documentiLavoro = [];
/** @var array<int, string> */
public array $descrizioneFile = ['', '', '', '', '', ''];
public string $messaggioVeloce = '';
public bool $lavoroStandard = false;
public bool $richiestaProforma = false;
public string $proformaCodice = '';
public string $proformaNote = '';
public string $qrToken = '';
public ?array $attachmentPreview = null;
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'fornitore']);
}
public function mount(int | string $record): void
{
$recordId = (int) $record;
abort_unless($recordId > 0, 404);
$user = Auth::user();
$isAdmin = $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore']);
if ($isAdmin) {
$intervento = $this->resolveInterventoRecord($recordId);
abort_unless($intervento instanceof TicketIntervento, 404);
[$fornitore, $dipendente] = $this->resolveOperatoreContext((int) $intervento->fornitore_id);
} else {
[$fornitore, $dipendente] = $this->resolveOperatoreContext();
abort_unless($fornitore instanceof Fornitore, 403, 'Profilo fornitore non collegato.');
$intervento = $this->resolveInterventoRecord($recordId, $fornitore, $dipendente);
abort_unless($intervento instanceof TicketIntervento, 404, 'Ticket/intervento non disponibile per questo fornitore.');
}
$this->intervento = $intervento;
$this->fornitore = $fornitore;
$this->dipendente = $dipendente;
$this->authorizeIntervento($this->intervento, $this->fornitore);
$this->authorizeDipendenteIntervento($this->intervento, $this->dipendente);
$this->reloadIntervento();
}
public function assignDipendente(): void
{
if ($this->dipendente instanceof FornitoreDipendente) {
Notification::make()->title('Assegnazione consentita solo al coordinatore fornitore')->warning()->send();
return;
}
$validated = $this->validate([
'dipendenteId' => ['nullable', 'integer'],
]);
$dipendente = null;
$selectedId = (int) ($validated['dipendenteId'] ?? 0);
if ($selectedId > 0) {
$dipendente = FornitoreDipendente::query()
->where('fornitore_id', (int) $this->fornitore->id)
->where('attivo', true)
->find($selectedId);
if (! $dipendente instanceof FornitoreDipendente) {
Notification::make()->title('Collaboratore non valido per questo fornitore')->danger()->send();
return;
}
}
$this->intervento->eseguito_da_dipendente_id = $dipendente?->id;
$this->intervento->save();
$stamp = now()->format('d/m/Y H:i');
$message = $dipendente
? '[' . $stamp . '] Intervento assegnato al collaboratore fornitore: ' . $dipendente->ruolo_operativo_label . '.'
: '[' . $stamp . '] Assegnazione collaboratore rimossa: intervento riportato al coordinamento del fornitore.';
$this->intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => $message,
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
$this->reloadIntervento();
Notification::make()->title('Assegnazione aggiornata')->success()->send();
}
public function startIntervento(): void
{
$this->intervento->update([
'stato' => 'in_corso',
'preso_in_carico_da_user_id' => Auth::id(),
'eseguito_da_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id,
'iniziato_at' => $this->intervento->iniziato_at ?? now(),
]);
$this->intervento->ticket->update(['stato' => 'In Lavorazione']);
$this->reloadIntervento();
Notification::make()->title('Intervento preso in carico')->success()->send();
}
public function saveRapporto(): void
{
$validated = $this->validate([
'rapportoFornitore' => ['required', 'string', 'max:5000'],
'tempoMinuti' => ['nullable', 'integer', 'min:1', 'max:1440'],
'articoliUtilizzati' => ['nullable', 'array'],
'articoliUtilizzati.*' => ['nullable', 'string', 'max:120'],
'fotoLavoro' => ['nullable', 'array', 'max:10'],
'fotoLavoro.*' => ['nullable', 'image', 'max:10240'],
'documentiLavoro' => ['nullable', 'array', 'max:10'],
'documentiLavoro.*' => ['nullable', 'file', 'max:10240', 'mimes:pdf,doc,docx,xls,xlsx,txt,jpg,jpeg,png,webp'],
'descrizioneFile' => ['nullable', 'array'],
'descrizioneFile.*' => ['nullable', 'string', 'max:255'],
'apparatoMarca' => ['nullable', 'string', 'max:120'],
'apparatoModello' => ['nullable', 'string', 'max:120'],
'apparatoSeriale' => ['nullable', 'string', 'max:120'],
'messaggioVeloce' => ['nullable', 'string', 'max:1500'],
'lavoroStandard' => ['nullable', 'boolean'],
'richiestaProforma' => ['nullable', 'boolean'],
'proformaCodice' => ['nullable', 'string', 'max:80'],
'proformaNote' => ['nullable', 'string', 'max:2000'],
'qrToken' => ['nullable', 'string', 'max:40'],
]);
$rapporto = trim((string) $validated['rapportoFornitore']);
$apparato = $this->buildApparatoSummary(
(string) ($validated['apparatoMarca'] ?? ''),
(string) ($validated['apparatoModello'] ?? ''),
(string) ($validated['apparatoSeriale'] ?? '')
);
if ($apparato !== '') {
$rapporto .= "\n\n" . $apparato;
}
$payload = [
'rapporto_fornitore' => $rapporto,
'tempo_minuti' => $validated['tempoMinuti'] ?? null,
'terminato_at' => now(),
'stato' => ! empty($validated['lavoroStandard']) ? 'fatturabile' : 'in_verifica',
'eseguito_da_dipendente_id' => $this->dipendente?->id ?? $this->intervento->eseguito_da_dipendente_id,
];
$articoli = collect($validated['articoliUtilizzati'] ?? [])
->map(fn($item) => trim((string) $item))
->filter()
->values()
->all();
if ($articoli !== []) {
$payload['articoli_utilizzati'] = $articoli;
}
if ($this->fotoLavoro !== []) {
$firstPhoto = $this->fotoLavoro[0] ?? null;
if ($firstPhoto) {
$payload['foto_path'] = $firstPhoto->store('ticket-interventi/foto/' . $this->intervento->id, 'public');
}
}
$allFiles = array_merge($this->fotoLavoro, $this->documentiLavoro);
if ($allFiles !== [] && Schema::hasTable('ticket_attachments')) {
foreach ($allFiles as $index => $file) {
if (! $file) {
continue;
}
$path = $file->store('ticket-interventi/allegati/' . $this->intervento->id, 'public');
TicketAttachment::query()->create([
'ticket_id' => (int) $this->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) ($this->descrizioneFile[$index] ?? '')) ?: 'Allegato rapporto fornitore',
]);
}
}
$tokenInserito = trim((string) ($validated['qrToken'] ?? ''));
if ($tokenInserito !== '' && strcasecmp($tokenInserito, (string) $this->intervento->qr_token) === 0) {
$payload['qr_scansionato_at'] = now();
}
$this->intervento->update($payload);
$stamp = now()->format('d/m/Y H:i');
$this->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['proformaCodice'] ?? ''));
$proformaNote = trim((string) ($validated['proformaNote'] ?? ''));
if (! empty($validated['richiestaProforma']) || $proformaCodice !== '' || $proformaNote !== '') {
$this->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['messaggioVeloce'] ?? ''));
if ($quickMessage !== '') {
$this->intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => '[' . $stamp . '] Messaggio veloce fornitore: ' . $quickMessage,
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
}
$this->fotoLavoro = [];
$this->documentiLavoro = [];
$this->descrizioneFile = ['', '', '', '', '', ''];
$this->messaggioVeloce = '';
$this->proformaCodice = '';
$this->proformaNote = '';
$this->dispatch('fornitore-local-previews-clear');
$this->reloadIntervento();
Notification::make()->title('Rapporto inviato in verifica amministratore')->success()->send();
}
public function inviaSollecito(): void
{
$stamp = now()->format('d/m/Y H:i');
$this->intervento->ticket->messages()->create([
'user_id' => Auth::id(),
'messaggio' => '[' . $stamp . '] Sollecito fornitore su ticket/intervento in corso.',
'canale' => 'interno',
'direzione' => 'inbound',
'inviato_il' => now(),
]);
$this->reloadIntervento();
Notification::make()->title('Sollecito inviato allo studio amministrativo')->success()->send();
}
public function getTicketsUrl(): string
{
return TicketOperativi::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
}
public function getAttachmentPublicUrl(TicketAttachment $attachment): string
{
return route('filament.tickets.attachments.view', ['attachment' => (int) $attachment->id]);
}
public function openAttachmentPreview(int $attachmentId): void
{
$attachment = $this->intervento->ticket->attachments->firstWhere('id', $attachmentId);
if (! $attachment instanceof TicketAttachment) {
return;
}
$this->attachmentPreview = [
'id' => (int) $attachment->id,
'name' => (string) ($attachment->original_file_name ?? 'allegato'),
'description' => (string) ($attachment->description ?? ''),
'url' => $this->getAttachmentPublicUrl($attachment),
'is_image' => $attachment->isImage(),
'is_pdf' => $attachment->isPdf(),
'mime' => $attachment->resolvedMimeType(),
];
}
public function closeAttachmentPreview(): void
{
$this->attachmentPreview = null;
}
public function getCollaboratoriUrl(): string
{
return Collaboratori::getUrl(['fornitore' => (int) $this->fornitore->id], panel: 'admin-filament');
}
public function canAssignDipendente(): bool
{
return ! ($this->dipendente instanceof FornitoreDipendente) && count($this->dipendentiOptions) > 0;
}
protected function resolveInterventoRecord(int $recordId, ?Fornitore $fornitore = null, ?FornitoreDipendente $dipendente = null): ?TicketIntervento
{
$query = TicketIntervento::query();
if ($fornitore instanceof Fornitore) {
$query->where('fornitore_id', (int) $fornitore->id);
}
if ($dipendente instanceof FornitoreDipendente) {
$query->where(function (Builder $builder) use ($dipendente): void {
$builder->whereNull('eseguito_da_dipendente_id')
->orWhere('eseguito_da_dipendente_id', (int) $dipendente->id);
});
}
return $query
->where(function (Builder $builder) use ($recordId): void {
$builder->whereKey($recordId)
->orWhere('ticket_id', $recordId);
})
->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [$recordId])
->latest('id')
->first();
}
protected function reloadIntervento(): void
{
$this->intervento->load([
'ticket.stabile',
'ticket.unitaImmobiliare',
'ticket.soggettoRichiedente',
'ticket.messages.user',
'ticket.attachments.user',
'ticket.apertoDaUser',
'ticket.assegnatoAUser',
'ticket.assegnatoAFornitore',
'eseguitoDaDipendente',
]);
$this->intervento->refresh();
$this->intervento->load([
'ticket.stabile',
'ticket.unitaImmobiliare',
'ticket.soggettoRichiedente',
'ticket.messages.user',
'ticket.attachments.user',
'ticket.apertoDaUser',
'ticket.assegnatoAUser',
'ticket.assegnatoAFornitore',
'eseguitoDaDipendente',
]);
$this->caller = $this->resolveCallerData($this->intervento->ticket);
$this->storicoRows = TicketIntervento::query()
->with(['ticket'])
->where('fornitore_id', (int) $this->fornitore->id)
->where('id', '!=', (int) $this->intervento->id)
->when(
(int) ($this->intervento->ticket?->soggetto_richiedente_id ?? 0) > 0,
fn($query) => $query->whereHas('ticket', fn($ticketQuery) => $ticketQuery->where('soggetto_richiedente_id', (int) $this->intervento->ticket->soggetto_richiedente_id)),
fn($query) => $query->whereHas('ticket', fn($ticketQuery) => $ticketQuery->where('stabile_id', (int) ($this->intervento->ticket?->stabile_id ?? 0)))
)
->latest('id')
->limit(12)
->get()
->map(fn(TicketIntervento $old) => [
'ticket_id' => (int) $old->ticket_id,
'titolo' => (string) ($old->ticket->titolo ?? '-'),
'stato' => (string) $old->stato,
'updated_at' => optional($old->updated_at)->format('d/m/Y H:i') ?: '-',
'note' => (string) ($old->note_amministratore ?? ''),
])
->all();
$this->dipendentiOptions = $this->fornitore->dipendenti()
->where('attivo', true)
->with('fornitoreEsterno')
->orderBy('cognome')
->orderBy('nome')
->get()
->map(fn(FornitoreDipendente $dipendente) => [
'id' => (int) $dipendente->id,
'label' => $dipendente->ruolo_operativo_label . ($dipendente->email ? ' · ' . $dipendente->email : ''),
])
->all();
$this->dipendenteId = (int) ($this->intervento->eseguito_da_dipendente_id ?? 0) ?: null;
$this->rapportoFornitore = (string) ($this->intervento->rapporto_fornitore ?? '');
$this->tempoMinuti = $this->intervento->tempo_minuti ? (int) $this->intervento->tempo_minuti : null;
$this->articoliUtilizzati = array_pad((array) ($this->intervento->articoli_utilizzati ?? []), 3, '');
$this->qrToken = '';
}
/**
* @return array{contatto:string,telefono:string,email:string,problema:string,sorgente:string,riferimento:string}
*/
protected function resolveCallerData(?Ticket $ticket): array
{
$parsed = $this->extractCallerData((string) ($ticket?->descrizione ?? ''));
$caller = [
'contatto' => $parsed['contatto'],
'telefono' => $parsed['telefono'],
'email' => '',
'problema' => $parsed['problema'] !== '' ? $parsed['problema'] : (string) ($ticket?->titolo ?? ''),
'sorgente' => 'Descrizione ticket',
'riferimento' => $this->buildCallerReference($ticket),
];
$soggetto = $ticket?->soggettoRichiedente;
if ($soggetto) {
$label = trim((string) ($soggetto->ragione_sociale ?: trim(($soggetto->nome ?? '') . ' ' . ($soggetto->cognome ?? ''))));
if ($label !== '') {
$caller['contatto'] = $label;
}
$caller['telefono'] = trim((string) ($soggetto->telefono ?? '')) ?: $caller['telefono'];
$caller['email'] = trim((string) ($soggetto->email ?? ''));
$caller['sorgente'] = 'Richiedente collegato al ticket';
return $caller;
}
$openedBy = $ticket?->apertoDaUser;
if ($openedBy instanceof User) {
$caller['contatto'] = trim((string) ($openedBy->name ?? '')) ?: $caller['contatto'];
$caller['email'] = trim((string) ($openedBy->email ?? ''));
$caller['sorgente'] = 'Utente che ha aperto il ticket';
}
return $caller;
}
protected function buildCallerReference(?Ticket $ticket): string
{
if (! $ticket instanceof Ticket) {
return '';
}
$parts = [];
$stabile = $ticket->stabile;
if ($stabile) {
$denominazione = trim((string) ($stabile->denominazione ?? ''));
$indirizzo = trim(implode(' ', array_filter([
$stabile->indirizzo ?? null,
$stabile->cap ?? null,
$stabile->citta ?? null,
])));
if ($denominazione !== '') {
$parts[] = $denominazione;
}
if ($indirizzo !== '') {
$parts[] = $indirizzo;
}
}
$luogoIntervento = trim((string) ($ticket->luogo_intervento ?? ''));
if ($luogoIntervento !== '') {
$parts[] = 'Luogo: ' . $luogoIntervento;
}
return implode(' · ', array_unique($parts));
}
}