1418 lines
54 KiB
PHP
1418 lines
54 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Supporto;
|
|
|
|
use App\Models\CategoriaTicket;
|
|
use App\Models\Documento;
|
|
use App\Models\Fornitore;
|
|
use App\Models\FornitoreDipendente;
|
|
use App\Models\InsuranceClaim;
|
|
use App\Models\InsurancePolicy;
|
|
use App\Models\Ticket;
|
|
use App\Models\TicketAttachment;
|
|
use App\Models\TicketIntervento;
|
|
use App\Models\User;
|
|
use App\Services\Documenti\ImageTextExtractionService;
|
|
use App\Services\Documenti\PdfTextExtractionService;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
use Livewire\WithFileUploads;
|
|
use UnitEnum;
|
|
|
|
class TicketGestione extends Page
|
|
{
|
|
use WithFileUploads;
|
|
|
|
/** @var array<int,string>|null */
|
|
private ?array $documentiColumnsCache = null;
|
|
|
|
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 $fornitoreSearch = '';
|
|
|
|
public ?string $noteAssegnazione = null;
|
|
|
|
public ?string $insurancePolicyReference = null;
|
|
|
|
public ?int $insurancePolicyId = null;
|
|
|
|
public ?string $insuranceClaimNumber = null;
|
|
|
|
public ?string $insuranceStatus = null;
|
|
|
|
public ?string $insuranceTakenInChargeAt = null;
|
|
|
|
public ?string $insuranceEmailSentAt = null;
|
|
|
|
public ?string $insuranceInsurerContactedAt = null;
|
|
|
|
public ?string $insuranceDocumentsSentAt = null;
|
|
|
|
public ?string $insuranceAppointmentAt = null;
|
|
|
|
public ?string $insuranceClosedAt = null;
|
|
|
|
public ?string $insuranceNextAction = null;
|
|
|
|
public ?string $insuranceNotes = null;
|
|
|
|
/** @var array<int,mixed> */
|
|
public array $nuoviAllegati = [];
|
|
|
|
/** @var array<int,mixed> */
|
|
public array $nuoveFoto = [];
|
|
|
|
public ?string $descrizioneAllegati = null;
|
|
|
|
public ?string $lastGeneratedPassword = null;
|
|
|
|
public ?int $selectedCategoriaId = null;
|
|
|
|
public ?string $categoriaNome = null;
|
|
|
|
public ?string $categoriaDescrizione = null;
|
|
|
|
/** @var array<int,array{id:int,nome:string,descrizione:string}> */
|
|
public array $categorieOptions = [];
|
|
|
|
/** @var array<int,array<string,mixed>> */
|
|
public array $fornitoreMatches = [];
|
|
|
|
/** @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->loadCategorieTicketOptions();
|
|
$this->refreshFornitoriAttiviRows();
|
|
if ($this->selectedTicket) {
|
|
$this->fornitoreId = (int) ($this->selectedTicket->assegnato_a_fornitore_id ?? 0) ?: null;
|
|
}
|
|
|
|
$this->refreshData();
|
|
}
|
|
|
|
public function updatedSelectedCategoriaId($value): void
|
|
{
|
|
$id = (int) $value;
|
|
if ($id <= 0) {
|
|
$this->categoriaNome = null;
|
|
$this->categoriaDescrizione = null;
|
|
return;
|
|
}
|
|
|
|
$categoria = CategoriaTicket::query()->find($id);
|
|
if (! $categoria instanceof CategoriaTicket) {
|
|
return;
|
|
}
|
|
|
|
$this->categoriaNome = (string) $categoria->nome;
|
|
$this->categoriaDescrizione = (string) ($categoria->descrizione ?? '');
|
|
}
|
|
|
|
public function creaCategoriaTicket(): void
|
|
{
|
|
$this->validate([
|
|
'categoriaNome' => ['required', 'string', 'max:255', Rule::unique('categorie_ticket', 'nome')],
|
|
'categoriaDescrizione' => ['nullable', 'string', 'max:2000'],
|
|
]);
|
|
|
|
$categoria = CategoriaTicket::query()->create([
|
|
'nome' => trim((string) $this->categoriaNome),
|
|
'descrizione' => filled($this->categoriaDescrizione) ? trim((string) $this->categoriaDescrizione) : null,
|
|
]);
|
|
|
|
$this->loadCategorieTicketOptions();
|
|
$this->selectedCategoriaId = (int) $categoria->id;
|
|
$this->updatedSelectedCategoriaId($this->selectedCategoriaId);
|
|
|
|
Notification::make()->title('Categoria creata')->success()->send();
|
|
}
|
|
|
|
public function aggiornaCategoriaTicket(): void
|
|
{
|
|
$id = (int) ($this->selectedCategoriaId ?? 0);
|
|
if ($id <= 0) {
|
|
Notification::make()->title('Seleziona una categoria da modificare')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'categoriaNome' => ['required', 'string', 'max:255', Rule::unique('categorie_ticket', 'nome')->ignore($id)],
|
|
'categoriaDescrizione' => ['nullable', 'string', 'max:2000'],
|
|
]);
|
|
|
|
$categoria = CategoriaTicket::query()->find($id);
|
|
if (! $categoria instanceof CategoriaTicket) {
|
|
Notification::make()->title('Categoria non trovata')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$categoria->update([
|
|
'nome' => trim((string) $this->categoriaNome),
|
|
'descrizione' => filled($this->categoriaDescrizione) ? trim((string) $this->categoriaDescrizione) : null,
|
|
]);
|
|
|
|
$this->loadCategorieTicketOptions();
|
|
Notification::make()->title('Categoria aggiornata')->success()->send();
|
|
$this->refreshData();
|
|
}
|
|
|
|
public function updatedStatus(): void
|
|
{
|
|
$this->refreshData();
|
|
}
|
|
|
|
public function updatedFornitoreSearch(): void
|
|
{
|
|
$this->searchFornitori();
|
|
}
|
|
|
|
public function refreshData(): void
|
|
{
|
|
$this->loadCounters();
|
|
$this->loadTickets();
|
|
$this->refreshFornitoriAttiviRows();
|
|
$this->syncSelectedTicketState();
|
|
}
|
|
|
|
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;
|
|
$this->syncSelectedTicketState();
|
|
}
|
|
|
|
public function apriElenco(): void
|
|
{
|
|
$this->activeTab = 'elenco';
|
|
}
|
|
|
|
public function getFornitoreOperativoUrl(?int $fornitoreId = null): string
|
|
{
|
|
$base = \App\Filament\Pages\Fornitore\TicketOperativi::getUrl(panel: 'admin-filament');
|
|
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('filament.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 = $attachment->resolvedMimeType();
|
|
|
|
$isImage = $attachment->isImage();
|
|
$isPdf = $attachment->isPdf();
|
|
|
|
$this->attachmentPreview = [
|
|
'id' => (int) $attachment->id,
|
|
'name' => $name,
|
|
'description' => (string) ($attachment->description ?? ''),
|
|
'url' => $this->getAttachmentPublicUrl($attachment),
|
|
'is_image' => $isImage,
|
|
'is_pdf' => $isPdf,
|
|
'mime' => $mime,
|
|
];
|
|
}
|
|
|
|
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',
|
|
'insuranceClaim',
|
|
'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 selezionaFornitore(int $fornitoreId): void
|
|
{
|
|
$match = collect($this->fornitoreMatches)->firstWhere('id', $fornitoreId);
|
|
|
|
if (! is_array($match)) {
|
|
$fornitore = $this->resolveFornitoriBaseQuery()->find($fornitoreId);
|
|
if (! $fornitore instanceof Fornitore) {
|
|
return;
|
|
}
|
|
|
|
$match = $this->mapFornitoreSearchRow($fornitore);
|
|
}
|
|
|
|
$this->fornitoreId = (int) $match['id'];
|
|
$this->fornitoreSearch = (string) $match['nome'];
|
|
$this->fornitoreMatches = [];
|
|
}
|
|
|
|
public function resetFornitoreSelection(): void
|
|
{
|
|
$this->fornitoreId = null;
|
|
$this->fornitoreSearch = '';
|
|
$this->fornitoreMatches = [];
|
|
}
|
|
|
|
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;
|
|
|
|
// Force refresh of selected ticket relations so the new note is visible immediately.
|
|
$this->refreshData();
|
|
|
|
Notification::make()->title('Nota aggiunta')->success()->send();
|
|
}
|
|
|
|
public function salvaSinistroAssicurativo(): void
|
|
{
|
|
$ticket = $this->selectedTicket;
|
|
if (! $ticket) {
|
|
Notification::make()->title('Ticket non trovato')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'insurancePolicyId' => ['nullable', 'integer', 'exists:insurance_policies,id'],
|
|
'insurancePolicyReference' => ['nullable', 'string', 'max:255'],
|
|
'insuranceClaimNumber' => ['nullable', 'string', 'max:255'],
|
|
'insuranceStatus' => ['nullable', 'string', 'max:50'],
|
|
'insuranceTakenInChargeAt' => ['nullable', 'date'],
|
|
'insuranceEmailSentAt' => ['nullable', 'date'],
|
|
'insuranceInsurerContactedAt' => ['nullable', 'date'],
|
|
'insuranceDocumentsSentAt' => ['nullable', 'date'],
|
|
'insuranceAppointmentAt' => ['nullable', 'date'],
|
|
'insuranceClosedAt' => ['nullable', 'date'],
|
|
'insuranceNextAction' => ['nullable', 'string', 'max:1000'],
|
|
'insuranceNotes' => ['nullable', 'string', 'max:4000'],
|
|
]);
|
|
|
|
$claimMetadata = array_merge((array) ($ticket->insuranceClaim?->metadata ?? []), [
|
|
'taken_in_charge_at' => $this->insuranceTakenInChargeAt ?: null,
|
|
'email_sent_at' => $this->insuranceEmailSentAt ?: null,
|
|
'insurer_contacted_at' => $this->insuranceInsurerContactedAt ?: null,
|
|
'documents_sent_at' => $this->insuranceDocumentsSentAt ?: null,
|
|
'appointment_at' => $this->insuranceAppointmentAt ?: null,
|
|
'closed_at' => $this->insuranceClosedAt ?: null,
|
|
'next_action' => filled($this->insuranceNextAction) ? trim((string) $this->insuranceNextAction) : null,
|
|
]);
|
|
|
|
$claim = InsuranceClaim::query()->updateOrCreate(
|
|
['ticket_id' => (int) $ticket->id],
|
|
[
|
|
'insurance_policy_id' => (int) ($this->insurancePolicyId ?? 0) ?: null,
|
|
'stabile_id' => (int) $ticket->stabile_id,
|
|
'policy_reference' => filled($this->insurancePolicyReference) ? trim((string) $this->insurancePolicyReference) : null,
|
|
'claim_number' => filled($this->insuranceClaimNumber) ? trim((string) $this->insuranceClaimNumber) : null,
|
|
'status' => filled($this->insuranceStatus) ? trim((string) $this->insuranceStatus) : 'aperta',
|
|
'opened_at' => $ticket->insuranceClaim?->opened_at ?? now(),
|
|
'notes' => filled($this->insuranceNotes) ? trim((string) $this->insuranceNotes) : null,
|
|
'metadata' => $claimMetadata,
|
|
]
|
|
);
|
|
|
|
$ticket->messages()->create([
|
|
'user_id' => Auth::id(),
|
|
'messaggio' => 'Sinistro assicurativo aggiornato. Riferimento sinistro: ' . ($claim->claim_number ?: 'da definire') . ' · Stato: ' . ($claim->status ?: 'aperta'),
|
|
'canale' => 'assicurazione',
|
|
'direzione' => 'outbound',
|
|
'inviato_il' => now(),
|
|
'metadata' => [
|
|
'insurance_claim_id' => (int) $claim->id,
|
|
],
|
|
]);
|
|
|
|
$this->refreshData();
|
|
|
|
Notification::make()->title('Sinistro assicurativo aggiornato')->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');
|
|
$mime = TicketAttachment::normalizeMimeType(
|
|
method_exists($file, 'getClientMimeType') ? (string) $file->getClientMimeType() : '',
|
|
method_exists($file, 'getClientOriginalName') ? (string) $file->getClientOriginalName() : '',
|
|
$path
|
|
);
|
|
|
|
$attachment = 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' => $mime,
|
|
'size' => (int) $file->getSize(),
|
|
'description' => $this->descrizioneAllegati ?: 'Allegato da gestione ticket',
|
|
]);
|
|
|
|
if ($attachment instanceof TicketAttachment) {
|
|
$this->archiveTicketAttachmentDocument($ticket, $attachment);
|
|
}
|
|
|
|
$saved++;
|
|
}
|
|
|
|
$this->nuoveFoto = [];
|
|
$this->nuoviAllegati = [];
|
|
$this->descrizioneAllegati = null;
|
|
$this->refreshData();
|
|
|
|
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');
|
|
}
|
|
|
|
public function abilitaAccessoFornitoreDaTicket(int $fornitoreId): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
Notification::make()->title('Stabile attivo non disponibile')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$stabile = \App\Models\Stabile::query()->find($stabileId);
|
|
$adminId = (int) ($stabile->amministratore_id ?? 0);
|
|
|
|
$fornitore = Fornitore::query()->find($fornitoreId);
|
|
if (! $fornitore instanceof Fornitore) {
|
|
Notification::make()->title('Fornitore non trovato')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
if ($adminId > 0 && (int) ($fornitore->amministratore_id ?? 0) > 0 && (int) ($fornitore->amministratore_id ?? 0) !== $adminId) {
|
|
Notification::make()->title('Fornitore non valido per questo stabile')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$email = trim((string) ($fornitore->email ?? ''));
|
|
if ($email === '') {
|
|
Notification::make()->title('Email fornitore mancante')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$dipendente = FornitoreDipendente::query()
|
|
->where('fornitore_id', (int) $fornitore->id)
|
|
->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])
|
|
->first();
|
|
|
|
if (! $dipendente instanceof FornitoreDipendente) {
|
|
$dipendente = FornitoreDipendente::query()->create([
|
|
'fornitore_id' => (int) $fornitore->id,
|
|
'nome' => (string) ($fornitore->ragione_sociale ?: ($fornitore->nome ?: 'Referente fornitore')),
|
|
'cognome' => $fornitore->ragione_sociale ? null : (($fornitore->cognome ?? null) ?: null),
|
|
'email' => $email,
|
|
'telefono' => $fornitore->telefono ?: $fornitore->cellulare,
|
|
'attivo' => true,
|
|
'created_by_user_id' => Auth::id(),
|
|
'updated_by_user_id' => Auth::id(),
|
|
]);
|
|
}
|
|
|
|
$this->abilitaAccessoDipendente($dipendente);
|
|
$this->refreshData();
|
|
}
|
|
|
|
public function resetPasswordFornitoreUser(int $userId): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
Notification::make()->title('Stabile attivo non disponibile')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$stabile = \App\Models\Stabile::query()->find($stabileId);
|
|
$adminId = (int) ($stabile->amministratore_id ?? 0);
|
|
|
|
$canReset = FornitoreDipendente::query()
|
|
->where('user_id', $userId)
|
|
->whereHas('fornitore', function ($q) use ($adminId): void {
|
|
if ($adminId > 0) {
|
|
$q->where('amministratore_id', $adminId);
|
|
}
|
|
})
|
|
->exists();
|
|
|
|
if (! $canReset) {
|
|
Notification::make()->title('Utente non autorizzato per reset')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$target = User::query()->find($userId);
|
|
if (! $target instanceof User) {
|
|
Notification::make()->title('Utente non trovato')->danger()->send();
|
|
return;
|
|
}
|
|
|
|
$newPassword = Str::random(12);
|
|
$target->password = Hash::make($newPassword);
|
|
$target->save();
|
|
|
|
$this->lastGeneratedPassword = $newPassword;
|
|
|
|
Notification::make()
|
|
->title('Password fornitore resettata')
|
|
->body('Nuova password temporanea: ' . $newPassword)
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
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', 'assegnatoAUser:id,name'])
|
|
->withCount('attachments')
|
|
->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 = [];
|
|
$accessRows = FornitoreDipendente::query()
|
|
->whereIn('fornitore_id', $fornitoriIds->all())
|
|
->get(['fornitore_id', 'user_id']);
|
|
|
|
$dipCountByFornitore = $accessRows
|
|
->groupBy('fornitore_id')
|
|
->map(fn($r) => $r->count());
|
|
|
|
$linkedUserByFornitore = $accessRows
|
|
->filter(fn($r) => (int) ($r->user_id ?? 0) > 0)
|
|
->groupBy('fornitore_id')
|
|
->map(fn($r) => (int) ($r->first()->user_id ?? 0));
|
|
|
|
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) ?? ''),
|
|
'dipendenti_count' => (int) ($dipCountByFornitore->get((string) $fornitore->id) ?? $dipCountByFornitore->get((int) $fornitore->id) ?? 0),
|
|
'linked_user_id' => (int) ($linkedUserByFornitore->get((string) $fornitore->id) ?? $linkedUserByFornitore->get((int) $fornitore->id) ?? 0),
|
|
];
|
|
}
|
|
|
|
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 abilitaAccessoDipendente(FornitoreDipendente $dipendente): void
|
|
{
|
|
$email = trim((string) ($dipendente->email ?? ''));
|
|
if ($email === '') {
|
|
Notification::make()->title('Email dipendente mancante')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$existingUser = User::query()->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first();
|
|
$created = false;
|
|
if (! $existingUser instanceof User) {
|
|
$generatedPassword = Str::random(12);
|
|
$existingUser = User::query()->create([
|
|
'name' => trim((string) ($dipendente->nome_completo ?: 'Utente fornitore')),
|
|
'email' => $email,
|
|
'password' => Hash::make($generatedPassword),
|
|
'email_verified_at' => now(),
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$this->lastGeneratedPassword = $generatedPassword;
|
|
$created = true;
|
|
}
|
|
|
|
$existingUser->assignRole('fornitore');
|
|
|
|
$dipendente->user_id = (int) $existingUser->id;
|
|
$dipendente->updated_by_user_id = Auth::id();
|
|
$dipendente->save();
|
|
|
|
$body = $created
|
|
? 'Accesso creato. Password temporanea: ' . ($this->lastGeneratedPassword ?? '(non disponibile)')
|
|
: 'Accesso attivato/aggiornato per utente esistente.';
|
|
|
|
Notification::make()->title('Accesso fornitore abilitato')->body($body)->success()->send();
|
|
}
|
|
|
|
private function searchFornitori(): void
|
|
{
|
|
$raw = trim($this->fornitoreSearch);
|
|
|
|
if ($raw === '') {
|
|
if ($this->fornitoreId) {
|
|
$selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId);
|
|
$this->fornitoreMatches = $selected instanceof Fornitore ? [$this->mapFornitoreSearchRow($selected)] : [];
|
|
return;
|
|
}
|
|
|
|
$this->fornitoreMatches = [];
|
|
return;
|
|
}
|
|
|
|
$digits = preg_replace('/\D+/', '', $raw) ?: '';
|
|
$needleText = '%' . mb_strtolower($raw) . '%';
|
|
$needleDigits = '%' . $digits . '%';
|
|
$canonicalNeedles = $this->normalizeSearchTags($raw);
|
|
|
|
$matches = $this->resolveFornitoriBaseQuery()
|
|
->where(function ($query) use ($needleText, $needleDigits, $digits, $canonicalNeedles): void {
|
|
$query->orWhereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$needleText])
|
|
->orWhereRaw("LOWER(COALESCE(nome, '')) LIKE ?", [$needleText])
|
|
->orWhereRaw("LOWER(COALESCE(cognome, '')) LIKE ?", [$needleText])
|
|
->orWhereRaw("LOWER(COALESCE(email, '')) LIKE ?", [$needleText])
|
|
->orWhereRaw("LOWER(COALESCE(tags, '')) LIKE ?", [$needleText])
|
|
->orWhereRaw("LOWER(COALESCE(note, '')) LIKE ?", [$needleText]);
|
|
|
|
foreach ($canonicalNeedles as $canonicalNeedle) {
|
|
$query->orWhereRaw("LOWER(COALESCE(tags, '')) LIKE ?", ['%' . $canonicalNeedle . '%']);
|
|
}
|
|
|
|
if ($digits !== '') {
|
|
$query->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needleDigits])
|
|
->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needleDigits]);
|
|
}
|
|
})
|
|
->limit(60)
|
|
->get();
|
|
|
|
$this->fornitoreMatches = $matches
|
|
->map(function (Fornitore $fornitore) use ($raw, $digits, $canonicalNeedles): array {
|
|
$row = $this->mapFornitoreSearchRow($fornitore);
|
|
$row['score'] = $this->scoreFornitoreMatch($row, $raw, $digits, $canonicalNeedles);
|
|
|
|
return $row;
|
|
})
|
|
->sortByDesc('score')
|
|
->take(12)
|
|
->values()
|
|
->map(function (array $row): array {
|
|
unset($row['score']);
|
|
|
|
return $row;
|
|
})
|
|
->all();
|
|
|
|
if ($this->fornitoreId && ! collect($this->fornitoreMatches)->contains('id', $this->fornitoreId)) {
|
|
$selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId);
|
|
if ($selected instanceof Fornitore) {
|
|
array_unshift($this->fornitoreMatches, $this->mapFornitoreSearchRow($selected));
|
|
$this->fornitoreMatches = array_slice($this->fornitoreMatches, 0, 12);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function resolveFornitoriBaseQuery()
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return Fornitore::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
$stabileId = StabileContext::resolveActiveStabileId($user);
|
|
if (! $stabileId) {
|
|
return Fornitore::query()->whereRaw('1 = 0');
|
|
}
|
|
|
|
$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 ($builder) use ($adminId): void {
|
|
$builder->where('amministratore_id', $adminId)->orWhereNull('amministratore_id');
|
|
});
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* @return array<string,mixed>
|
|
*/
|
|
private function mapFornitoreSearchRow(Fornitore $fornitore): array
|
|
{
|
|
$tags = $this->splitFornitoreTags((string) ($fornitore->tags ?? ''));
|
|
$label = trim((string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? ''))));
|
|
|
|
return [
|
|
'id' => (int) $fornitore->id,
|
|
'nome' => $label !== '' ? $label : ('Fornitore #' . $fornitore->id),
|
|
'email' => (string) ($fornitore->email ?? ''),
|
|
'telefono' => (string) ($fornitore->telefono ?: $fornitore->cellulare),
|
|
'tags' => $tags,
|
|
'tags_label' => $tags !== [] ? implode(', ', $tags) : '',
|
|
'note' => Str::limit(trim((string) ($fornitore->note ?? '')), 120),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $row
|
|
* @param array<int,string> $canonicalNeedles
|
|
*/
|
|
private function scoreFornitoreMatch(array $row, string $raw, string $digits, array $canonicalNeedles): int
|
|
{
|
|
$score = (int) (($row['id'] ?? 0) === (int) ($this->fornitoreId ?? 0) ? 1000 : 0);
|
|
$haystack = mb_strtolower(implode(' ', array_filter([
|
|
(string) ($row['nome'] ?? ''),
|
|
(string) ($row['email'] ?? ''),
|
|
(string) ($row['telefono'] ?? ''),
|
|
(string) ($row['tags_label'] ?? ''),
|
|
(string) ($row['note'] ?? ''),
|
|
])));
|
|
|
|
if ($raw !== '' && str_contains($haystack, mb_strtolower($raw))) {
|
|
$score += 60;
|
|
}
|
|
|
|
if ($digits !== '') {
|
|
$phoneDigits = preg_replace('/\D+/', '', (string) ($row['telefono'] ?? '')) ?: '';
|
|
if ($phoneDigits !== '' && str_contains($phoneDigits, $digits)) {
|
|
$score += 45;
|
|
}
|
|
}
|
|
|
|
$rowTags = array_map(fn(string $tag): string => mb_strtolower($tag), (array) ($row['tags'] ?? []));
|
|
foreach ($canonicalNeedles as $canonicalNeedle) {
|
|
foreach ($rowTags as $rowTag) {
|
|
if ($rowTag === $canonicalNeedle) {
|
|
$score += 90;
|
|
} elseif (str_contains($rowTag, $canonicalNeedle) || str_contains($canonicalNeedle, $rowTag)) {
|
|
$score += 55;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $score;
|
|
}
|
|
|
|
/**
|
|
* @return array<int,string>
|
|
*/
|
|
private function normalizeSearchTags(string $input): array
|
|
{
|
|
$tags = [];
|
|
foreach ($this->splitFornitoreTags($input) as $tag) {
|
|
$normalized = $this->canonicalizeFornitoreTag($tag);
|
|
if ($normalized !== null) {
|
|
$tags[] = $normalized;
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($tags));
|
|
}
|
|
|
|
/**
|
|
* @return array<int,string>
|
|
*/
|
|
private function splitFornitoreTags(string $value): array
|
|
{
|
|
$parts = preg_split('/[,;|\n\r\/]+/', $value) ?: [];
|
|
|
|
return array_values(array_filter(array_map(function (string $part): string {
|
|
$clean = trim($part);
|
|
$clean = preg_replace('/\s+/', ' ', $clean) ?? '';
|
|
|
|
return $clean;
|
|
}, $parts), fn(string $part): bool => $part !== ''));
|
|
}
|
|
|
|
private function canonicalizeFornitoreTag(string $raw): ?string
|
|
{
|
|
$clean = trim(mb_strtolower($raw));
|
|
if ($clean === '' || mb_strlen($clean) < 3) {
|
|
return null;
|
|
}
|
|
|
|
$map = [
|
|
'idr' => 'idraulico',
|
|
'idraul' => 'idraulico',
|
|
'elett' => 'elettricista',
|
|
'elettric' => 'elettricista',
|
|
'ascens' => 'ascensorista',
|
|
'puliz' => 'pulizie',
|
|
'giardin' => 'giardiniere',
|
|
'assicur' => 'assicurazione',
|
|
'manut' => 'manutenzione',
|
|
'spurgh' => 'spurghi',
|
|
'fogn' => 'spurghi',
|
|
'serr' => 'serrature',
|
|
'cald' => 'caldaia',
|
|
];
|
|
|
|
foreach ($map as $prefix => $normalized) {
|
|
if (str_starts_with($clean, $prefix)) {
|
|
return $normalized;
|
|
}
|
|
}
|
|
|
|
return $clean;
|
|
}
|
|
|
|
private function loadCategorieTicketOptions(): void
|
|
{
|
|
if (! Schema::hasTable('categorie_ticket')) {
|
|
$this->categorieOptions = [];
|
|
return;
|
|
}
|
|
|
|
$this->categorieOptions = CategoriaTicket::query()
|
|
->orderBy('nome')
|
|
->get(['id', 'nome', 'descrizione'])
|
|
->map(fn(CategoriaTicket $c): array=> [
|
|
'id' => (int) $c->id,
|
|
'nome' => (string) $c->nome,
|
|
'descrizione' => (string) ($c->descrizione ?? ''),
|
|
])
|
|
->all();
|
|
}
|
|
|
|
public function getTipoInterventoLabel(Ticket $ticket): string
|
|
{
|
|
$categoria = trim((string) optional($ticket->categoriaTicket)->nome);
|
|
$titolo = trim((string) ($ticket->titolo ?? ''));
|
|
|
|
if ($categoria !== '' && $titolo !== '') {
|
|
return $categoria . ' - ' . $titolo;
|
|
}
|
|
|
|
if ($categoria !== '') {
|
|
return $categoria;
|
|
}
|
|
|
|
if ($titolo !== '') {
|
|
return $titolo;
|
|
}
|
|
|
|
return 'N/D';
|
|
}
|
|
|
|
private function syncSelectedTicketState(): void
|
|
{
|
|
$ticket = $this->selectedTicket;
|
|
|
|
if (! $ticket) {
|
|
$this->fornitoreSearch = '';
|
|
$this->fornitoreMatches = [];
|
|
$this->insurancePolicyId = null;
|
|
$this->insurancePolicyReference = null;
|
|
$this->insuranceClaimNumber = null;
|
|
$this->insuranceStatus = null;
|
|
$this->insuranceTakenInChargeAt = null;
|
|
$this->insuranceEmailSentAt = null;
|
|
$this->insuranceInsurerContactedAt = null;
|
|
$this->insuranceDocumentsSentAt = null;
|
|
$this->insuranceAppointmentAt = null;
|
|
$this->insuranceClosedAt = null;
|
|
$this->insuranceNextAction = null;
|
|
$this->insuranceNotes = null;
|
|
return;
|
|
}
|
|
|
|
$this->fornitoreId = (int) ($ticket->assegnato_a_fornitore_id ?? 0) ?: null;
|
|
if ($this->fornitoreId) {
|
|
$selected = $this->resolveFornitoriBaseQuery()->find((int) $this->fornitoreId);
|
|
if ($selected instanceof Fornitore) {
|
|
$this->fornitoreSearch = $this->mapFornitoreSearchRow($selected)['nome'];
|
|
$this->fornitoreMatches = [$this->mapFornitoreSearchRow($selected)];
|
|
}
|
|
} else {
|
|
$this->fornitoreSearch = '';
|
|
$this->fornitoreMatches = [];
|
|
}
|
|
|
|
$this->insurancePolicyId = (int) ($ticket->insuranceClaim?->insurance_policy_id ?? 0) ?: null;
|
|
$this->insurancePolicyReference = (string) ($ticket->insuranceClaim?->policy_reference ?? '');
|
|
$this->insuranceClaimNumber = (string) ($ticket->insuranceClaim?->claim_number ?? '');
|
|
$this->insuranceStatus = (string) ($ticket->insuranceClaim?->status ?? 'aperta');
|
|
$this->insuranceTakenInChargeAt = filled(data_get($ticket->insuranceClaim?->metadata, 'taken_in_charge_at')) ? (string) data_get($ticket->insuranceClaim?->metadata, 'taken_in_charge_at') : null;
|
|
$this->insuranceEmailSentAt = filled(data_get($ticket->insuranceClaim?->metadata, 'email_sent_at')) ? (string) data_get($ticket->insuranceClaim?->metadata, 'email_sent_at') : null;
|
|
$this->insuranceInsurerContactedAt = filled(data_get($ticket->insuranceClaim?->metadata, 'insurer_contacted_at')) ? (string) data_get($ticket->insuranceClaim?->metadata, 'insurer_contacted_at') : null;
|
|
$this->insuranceDocumentsSentAt = filled(data_get($ticket->insuranceClaim?->metadata, 'documents_sent_at')) ? (string) data_get($ticket->insuranceClaim?->metadata, 'documents_sent_at') : null;
|
|
$this->insuranceAppointmentAt = filled(data_get($ticket->insuranceClaim?->metadata, 'appointment_at')) ? (string) data_get($ticket->insuranceClaim?->metadata, 'appointment_at') : null;
|
|
$this->insuranceClosedAt = filled(data_get($ticket->insuranceClaim?->metadata, 'closed_at')) ? (string) data_get($ticket->insuranceClaim?->metadata, 'closed_at') : null;
|
|
$this->insuranceNextAction = (string) (data_get($ticket->insuranceClaim?->metadata, 'next_action') ?? '');
|
|
$this->insuranceNotes = (string) ($ticket->insuranceClaim?->notes ?? '');
|
|
|
|
$this->syncTicketAttachmentsArchive($ticket);
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
public function getInsurancePolicyOptionsProperty(): array
|
|
{
|
|
$ticket = $this->selectedTicket;
|
|
if (! $ticket || ! class_exists(InsurancePolicy::class)) {
|
|
return [];
|
|
}
|
|
|
|
return InsurancePolicy::query()
|
|
->where('stabile_id', (int) $ticket->stabile_id)
|
|
->orderByDesc('renewal_at')
|
|
->orderByDesc('expires_at')
|
|
->orderBy('policy_number')
|
|
->get()
|
|
->mapWithKeys(function (InsurancePolicy $policy): array {
|
|
$label = trim(implode(' · ', array_filter([
|
|
$policy->display_name,
|
|
$policy->policy_number,
|
|
$policy->expires_at?->format('d/m/Y'),
|
|
])));
|
|
|
|
return [(int) $policy->id => ($label !== '' ? $label : ('Polizza #' . (int) $policy->id))];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
private function syncTicketAttachmentsArchive(Ticket $ticket): void
|
|
{
|
|
foreach ($ticket->attachments as $attachment) {
|
|
if ($attachment instanceof TicketAttachment) {
|
|
$this->archiveTicketAttachmentDocument($ticket, $attachment);
|
|
}
|
|
}
|
|
}
|
|
|
|
private function archiveTicketAttachmentDocument(Ticket $ticket, TicketAttachment $attachment): void
|
|
{
|
|
if (! Schema::hasTable('documenti')) {
|
|
return;
|
|
}
|
|
|
|
$publicPath = trim((string) ($attachment->file_path ?? ''));
|
|
if ($publicPath === '' || ! Storage::disk('public')->exists($publicPath)) {
|
|
return;
|
|
}
|
|
|
|
$extension = strtolower((string) pathinfo((string) ($attachment->original_file_name ?: $publicPath), PATHINFO_EXTENSION));
|
|
$safeName = Str::slug(pathinfo((string) ($attachment->original_file_name ?: 'allegato'), PATHINFO_FILENAME));
|
|
$safeName = $safeName !== '' ? $safeName : ('attachment-' . (int) $attachment->id);
|
|
$archiveReference = $this->buildTicketArchiveReference($ticket, $attachment);
|
|
$localPath = $this->buildTicketArchivePath($ticket, $attachment, $safeName, $extension, $archiveReference);
|
|
|
|
if (! Storage::disk('local')->exists($localPath)) {
|
|
Storage::disk('local')->put($localPath, Storage::disk('public')->get($publicPath));
|
|
}
|
|
|
|
$contents = Storage::disk('local')->get($localPath);
|
|
$payload = [
|
|
'stabile_id' => (int) $ticket->stabile_id,
|
|
'utente_id' => Auth::id(),
|
|
'nome' => (string) ($attachment->original_file_name ?: ('Allegato ticket #' . (int) $attachment->id)),
|
|
'nome_file' => (string) ($attachment->original_file_name ?: basename($localPath)),
|
|
'percorso_file' => $localPath,
|
|
'tipo_documento' => 'ticket_allegato',
|
|
'tipologia' => $attachment->isImage() ? 'foto' : ($attachment->isPdf() ? 'comunicazione' : 'altro'),
|
|
'mime_type' => $attachment->resolvedMimeType(),
|
|
'descrizione' => (string) ($attachment->description ?: 'Allegato ticket #' . (int) $ticket->id),
|
|
'note' => 'Ticket #' . (int) $ticket->id . ' · ' . (string) ($ticket->titolo ?: 'senza titolo'),
|
|
'dimensione_file' => (int) ($attachment->size ?? strlen($contents)),
|
|
'estensione' => $extension !== '' ? $extension : null,
|
|
'hash_file' => hash('sha256', $contents),
|
|
'xml_data' => [
|
|
'ticket_id' => (int) $ticket->id,
|
|
'ticket_attachment_id' => (int) $attachment->id,
|
|
'source_public_path' => $publicPath,
|
|
'archive_reference' => $archiveReference,
|
|
'archive_year' => $this->resolveTicketArchiveYear($ticket, $attachment),
|
|
'archive_scope' => 'ticket',
|
|
'visibility_scope' => 'interno',
|
|
'visibility_groups' => ['supporto', 'amministrazione', 'stabile:' . (int) $ticket->stabile_id],
|
|
],
|
|
'metadati_ocr' => $attachment->isImage()
|
|
? ['status' => 'pending_image_ocr', 'source' => 'ticket_attachment']
|
|
: ['source' => 'ticket_attachment'],
|
|
'data_upload' => now(),
|
|
'approvato' => true,
|
|
'archiviato' => true,
|
|
'urgente' => false,
|
|
'is_demo' => false,
|
|
'archive_reference' => $archiveReference,
|
|
'visibility_scope' => 'interno',
|
|
'visibility_groups' => ['supporto', 'amministrazione', 'stabile:' . (int) $ticket->stabile_id],
|
|
'visibility_roles' => ['super-admin', 'admin', 'amministratore', 'collaboratore'],
|
|
];
|
|
|
|
$documento = Documento::query()->updateOrCreate(
|
|
[
|
|
'documentable_type' => Ticket::class,
|
|
'documentable_id' => (int) $ticket->id,
|
|
'path_file' => $localPath,
|
|
],
|
|
$this->filterDocumentoPayload($payload)
|
|
);
|
|
|
|
if ($attachment->isPdf()) {
|
|
app(PdfTextExtractionService::class)->extractAndStore($documento, false);
|
|
} elseif ($attachment->isImage()) {
|
|
app(ImageTextExtractionService::class)->extractAndStore($documento, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<int,string>
|
|
*/
|
|
private function documentiColumns(): array
|
|
{
|
|
if ($this->documentiColumnsCache === null) {
|
|
$this->documentiColumnsCache = Schema::hasTable('documenti')
|
|
? Schema::getColumnListing('documenti')
|
|
: [];
|
|
}
|
|
|
|
return $this->documentiColumnsCache;
|
|
}
|
|
|
|
private function filterDocumentoPayload(array $payload): array
|
|
{
|
|
$allowed = array_flip($this->documentiColumns());
|
|
return array_intersect_key($payload, $allowed);
|
|
}
|
|
|
|
private function buildTicketArchiveReference(Ticket $ticket, TicketAttachment $attachment): string
|
|
{
|
|
return sprintf('TKT-%06d-ATT-%06d', (int) $ticket->id, (int) $attachment->id);
|
|
}
|
|
|
|
private function buildTicketArchivePath(Ticket $ticket, TicketAttachment $attachment, string $safeName, string $extension, string $archiveReference): string
|
|
{
|
|
$year = $this->resolveTicketArchiveYear($ticket, $attachment);
|
|
$suffix = $extension !== '' ? ('.' . $extension) : '';
|
|
|
|
return 'documenti/' . $year
|
|
. '/stabili/stabile-' . (int) $ticket->stabile_id
|
|
. '/ticket/ticket-' . str_pad((string) (int) $ticket->id, 6, '0', STR_PAD_LEFT)
|
|
. '/' . Str::lower($archiveReference) . '-' . $safeName . $suffix;
|
|
}
|
|
|
|
private function resolveTicketArchiveYear(Ticket $ticket, TicketAttachment $attachment): string
|
|
{
|
|
$date = $attachment->created_at ?? $ticket->created_at ?? now();
|
|
return method_exists($date, 'format') ? $date->format('Y') : now()->format('Y');
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|