*/ public array $newTicketAttachments = []; public ?string $newTicketFotoNote = null; /** @var \Illuminate\Support\Collection */ public $tickets; /** @var \Illuminate\Support\Collection */ public $callerMatches; /** @var array */ public array $ticketCounters = [ 'open' => 0, 'urgent' => 0, 'closed' => 0, 'all' => 0, ]; /** @var array */ public array $categorieTicketOptions = []; /** @var array */ public array $interventiPreimpostati = [ 'idraulico_perdita' => 'Idraulico - Perdita acqua', 'elettrico_luci' => 'Elettrico - Luci/Quadro', 'ascensore_bloccato' => 'Ascensore - Bloccato', 'citofono_guasto' => 'Citofono - Guasto', 'pulizia_straordinaria' => 'Pulizia - Straordinaria', 'infissi_serrature' => 'Infissi/Serrature', 'fognatura' => 'Fognatura/Odori', 'verde_potatura' => 'Verde - Potatura', 'altro' => 'Altro', ]; 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->callerMatches = collect(); $this->newTicketPriorita = 'Media'; $this->newTicketTipoIntervento = 'altro'; $this->loadCategorieTicketOptions(); if (request()->query('status') === 'all') { $this->status = 'all'; } $this->refreshData(); } public function updatedStatus(): void { $this->refreshData(); } public function updatedCallerSearch(): void { $this->searchCaller(); } public function refreshData(): void { $this->loadCounters(); $this->loadTickets(); $this->searchCaller(); } public function getRubricaFilamentUrl(): string { return route('filament.admin-filament.pages.gescon.anagrafica.rubrica-universale'); } public function getDashboardFilamentUrl(): string { return route('filament.admin-filament.pages.dashboard'); } public function getTicketArchivioUrl(): string { return TicketGestione::getUrl(panel: 'admin-filament', parameters: ['status' => 'all']); } public function getTicketDettaglioUrl(int $ticketId): string { return TicketScheda::getUrl(panel: 'admin-filament') . '?ticket=' . $ticketId; } 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']) ->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(30)->get(); } 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 searchCaller(): void { $raw = trim($this->callerSearch); if ($raw === '') { $this->callerMatches = collect(); $this->selectedCallerId = null; return; } $digits = preg_replace('/\D+/', '', $raw) ?: ''; $needleText = '%' . mb_strtolower($raw) . '%'; $needle = '%' . $digits . '%'; $this->callerMatches = RubricaUniversale::query() ->where(function ($q) use ($needle, $needleText, $digits): void { $q->orWhereRaw("LOWER(COALESCE(nome, '')) LIKE ?", [$needleText]) ->orWhereRaw("LOWER(COALESCE(cognome, '')) LIKE ?", [$needleText]) ->orWhereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$needleText]) ->orWhereRaw("LOWER(COALESCE(email, '')) LIKE ?", [$needleText]); if ($digits !== '') { $q->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]) ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needle]); } }) ->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [(int) ($this->selectedCallerId ?? 0)]) ->orderBy('nome') ->orderBy('cognome') ->limit(12) ->get(); if ($this->selectedCallerId && ! $this->callerMatches->contains('id', $this->selectedCallerId)) { $this->selectedCallerId = null; } } public function selezionaCaller(int $rubricaId): void { $match = $this->callerMatches->firstWhere('id', $rubricaId); if (! $match) { $match = RubricaUniversale::query()->find($rubricaId); } if (! $match) { return; } $this->selectedCallerId = (int) $match->id; $this->callerSearch = trim((string) ($match->nome_completo ?: $match->ragione_sociale ?: '')); if (blank($this->newTicketTitolo)) { $this->newTicketTitolo = 'Segnalazione da ' . ($match->nome_completo ?: $match->ragione_sociale ?: 'contatto'); } $this->searchCaller(); } public function creaTicketRapido(): void { $this->validate([ 'newTicketTitolo' => ['required', 'string', 'max:255'], 'newTicketDescrizione' => ['required', 'string'], 'newTicketLuogo' => ['nullable', 'string', 'max:255'], 'newTicketPriorita' => ['required', 'in:Bassa,Media,Alta,Urgente'], 'newTicketCategoriaId' => ['nullable', 'integer', 'exists:categorie_ticket,id'], 'newTicketTipoIntervento' => ['nullable', 'string', 'max:80'], 'newTicketFotoNote' => ['nullable', 'string', 'max:2000'], 'newTicketAttachments' => ['nullable', 'array', 'max:8'], 'newTicketAttachments.*' => ['file', 'max:10240', 'mimes:jpg,jpeg,png,webp,pdf,doc,docx,xls,xlsx,txt'], ]); $user = Auth::user(); if (! $user instanceof User) { return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make() ->title('Seleziona prima uno stabile attivo') ->danger() ->send(); return; } $caller = null; if ($this->selectedCallerId) { $caller = RubricaUniversale::query()->find($this->selectedCallerId); } $descrizione = trim((string) $this->newTicketDescrizione); if ($caller) { $telefono = $caller->telefono_cellulare ?: ($caller->telefono_ufficio ?: $caller->telefono_casa); $descrizione .= "\n\nChiamante selezionato: " . ($caller->nome_completo ?: $caller->ragione_sociale ?: ('Rubrica #' . $caller->id)); if ($telefono) { $descrizione .= "\nTelefono: {$telefono}"; } if ($caller->tipo_utenza_call) { $descrizione .= "\nProfilo: {$caller->tipo_utenza_call}"; } if ($caller->riferimento_stabile) { $descrizione .= "\nRif. stabile: {$caller->riferimento_stabile}"; } if ($caller->riferimento_unita) { $descrizione .= "\nRif. unità: {$caller->riferimento_unita}"; } } if (filled($this->newTicketFotoNote)) { $descrizione .= "\n\nNota fotografica: " . trim((string) $this->newTicketFotoNote); } if (filled($this->newTicketTipoIntervento)) { $label = $this->interventiPreimpostati[(string) $this->newTicketTipoIntervento] ?? (string) $this->newTicketTipoIntervento; $descrizione .= "\nTipo intervento: " . $label; } $ticket = Ticket::query()->create([ 'stabile_id' => $stabileId, 'aperto_da_user_id' => $user->id, 'titolo' => (string) $this->newTicketTitolo, 'descrizione' => $descrizione, 'categoria_ticket_id' => $this->newTicketCategoriaId, 'luogo_intervento' => $this->newTicketLuogo, 'data_apertura' => now(), 'stato' => 'Aperto', 'priorita' => $this->newTicketPriorita, ]); $savedAttachments = $this->salvaAllegatiTicket($ticket, $user->id); Notification::make() ->title('Ticket creato') ->body( $savedAttachments > 0 ? 'Ticket #' . $ticket->id . ' creato con successo con ' . $savedAttachments . ' allegati.' : 'Ticket #' . $ticket->id . ' creato con successo' ) ->success() ->send(); $this->newTicketTitolo = null; $this->newTicketDescrizione = null; $this->newTicketLuogo = null; $this->newTicketPriorita = 'Media'; $this->newTicketCategoriaId = null; $this->newTicketTipoIntervento = 'altro'; $this->newTicketAttachments = []; $this->newTicketFotoNote = null; $this->refreshData(); } private function loadCategorieTicketOptions(): void { $this->categorieTicketOptions = CategoriaTicket::query() ->orderBy('nome') ->limit(200) ->get(['id', 'nome']) ->map(fn(CategoriaTicket $cat): array=> [ 'id' => (int) $cat->id, 'nome' => (string) $cat->nome, ]) ->all(); } private function salvaAllegatiTicket(Ticket $ticket, int $userId): int { if (! Schema::hasTable('ticket_attachments')) { return 0; } $saved = 0; foreach ($this->newTicketAttachments as $file) { if (! is_object($file) || ! method_exists($file, 'store')) { continue; } try { $path = $file->store('ticket-mobile/' . $ticket->id, 'public'); TicketAttachment::query()->create([ 'ticket_id' => (int) $ticket->id, 'ticket_update_id' => null, 'user_id' => $userId, 'file_path' => $path, 'original_file_name' => (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : basename($path)), 'mime_type' => (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream'), 'size' => (int) (method_exists($file, 'getSize') ? $file->getSize() : 0), 'description' => filled($this->newTicketFotoNote) ? ('Nota foto: ' . trim((string) $this->newTicketFotoNote)) : 'Allegato da Ticket Mobile', ]); $saved++; } catch (Throwable $e) { Log::warning('TicketMobile attachment save failed', [ 'ticket_id' => $ticket->id, 'error' => $e->getMessage(), ]); } } return $saved; } public function excerpt(?string $text, int $limit = 160): string { return Str::limit((string) $text, $limit); } public function getCallerStabileLabel(int $rubricaId): ?string { $stabile = Stabile::query()->where('rubrica_id', $rubricaId)->first(); if (! $stabile) { return null; } return $stabile->denominazione ?: ('Stabile #' . $stabile->id); } 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(); } }