*/ public $suggerimentiRubrica; public function mount(): void { $this->priorita = 'Media'; $this->direzione = 'in_arrivo'; $this->suggerimentiRubrica = collect(); $focus = (int) request()->query('focus_post_it', 0); $this->focusPostItId = $focus > 0 ? $focus : null; $this->selectedPostItId = $this->focusPostItId; $this->applyQueryPrefill(); } public function getGestionePostItUrl(): string { return PostItGestione::getUrl(panel: 'admin-filament'); } public function getGestioneTicketUrl(): string { return \App\Filament\Pages\Supporto\TicketGestione::getUrl(panel: 'admin-filament'); } public function selectPostIt(int $postItId): void { $this->selectedPostItId = $postItId; $this->loadSelectedPostItAssignment(); } public function assegnaPostIt(): void { if (! $this->isPostItTableReady()) { return; } $postIt = $this->selectedPostIt; if (! $postIt) { Notification::make()->title('Seleziona prima un Post-it')->warning()->send(); return; } $this->validate([ 'assegnazioneTipo' => ['required', 'in:operatore,collaboratore,fornitore'], 'assegnazioneUserId' => ['nullable', 'integer', 'exists:users,id'], 'assegnazioneFornitoreId' => ['nullable', 'integer', 'exists:fornitori,id'], ]); $payload = [ 'assegnazione_tipo' => $this->assegnazioneTipo, 'assegnato_a_user_id' => null, 'assegnato_a_fornitore_id' => null, ]; if (in_array($this->assegnazioneTipo, ['operatore', 'collaboratore'], true)) { if (! $this->assegnazioneUserId) { Notification::make()->title('Seleziona un utente')->warning()->send(); return; } $payload['assegnato_a_user_id'] = $this->assegnazioneUserId; } if ($this->assegnazioneTipo === 'fornitore') { if (! $this->assegnazioneFornitoreId) { Notification::make()->title('Seleziona un fornitore')->warning()->send(); return; } $payload['assegnato_a_fornitore_id'] = $this->assegnazioneFornitoreId; } $postIt->update($payload); if ($postIt->ticket) { $ticketPayload = []; if ($this->assegnazioneTipo === 'fornitore') { $ticketPayload['assegnato_a_fornitore_id'] = $this->assegnazioneFornitoreId; $ticketPayload['assegnato_a_user_id'] = null; } else { $ticketPayload['assegnato_a_user_id'] = $this->assegnazioneUserId; } if ($ticketPayload !== []) { $postIt->ticket->update($ticketPayload); } if (method_exists($postIt->ticket, 'messages') && Schema::hasTable('ticket_messages')) { $postIt->ticket->messages()->create([ 'user_id' => Auth::id(), 'messaggio' => 'Assegnazione da Post-it: ' . $this->getSelectedAssignmentLabel(), ]); } } Notification::make()->title('Assegnazione Post-it aggiornata')->success()->send(); $this->selectedPostItId = (int) $postIt->id; } public function isPostItTableReady(): bool { return Schema::hasTable('chiamate_post_it'); } public function isPostItAssignmentReady(): bool { return $this->isPostItTableReady() && Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo') && Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id') && Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id'); } public function updatedRicercaChiamante(?string $value): void { $query = trim((string) $value); if ($query === '') { $this->suggerimentiRubrica = collect(); $this->selectedSuggerimentoId = null; return; } $this->suggerimentiRubrica = $this->searchRubrica($query); if ($this->selectedSuggerimentoId && ! $this->suggerimentiRubrica->contains('id', $this->selectedSuggerimentoId)) { $this->selectedSuggerimentoId = null; } } public function updatedTelefono(?string $value): void { $input = trim((string) $value); if ($input === '') { $this->rubricaId = null; $this->stabileId = null; return; } $match = $this->findRubricaByPhone($input); if (! $match) { return; } $this->rubricaId = $match->id; $this->selectedSuggerimentoId = $match->id; if (blank($this->nomeChiamante)) { $this->nomeChiamante = $match->nome_completo ?: $match->ragione_sociale; } if (! $this->disableAutoStabileAssociation) { $stabile = Stabile::query()->where('rubrica_id', $match->id)->first(); if ($stabile) { $this->stabileId = $stabile->id; } } if (blank($this->ricercaChiamante)) { $this->ricercaChiamante = $match->nome_completo ?: $match->ragione_sociale; } } public function selezionaSuggerimentoRubrica(int $rubricaId): void { $match = $this->suggerimentiRubrica->firstWhere('id', $rubricaId); if (! $match) { $match = RubricaUniversale::query()->find($rubricaId); } if (! $match) { return; } $this->selectedSuggerimentoId = (int) $match->id; $this->rubricaId = (int) $match->id; $this->nomeChiamante = $match->nome_completo ?: $match->ragione_sociale; $this->telefono = $match->telefono_cellulare ?: ($match->telefono_ufficio ?: $match->telefono_casa); $this->ricercaChiamante = $match->nome_completo ?: ($match->ragione_sociale ?: ''); if (! $this->disableAutoStabileAssociation) { $stabile = Stabile::query()->where('rubrica_id', $match->id)->first(); $this->stabileId = $stabile?->id; } $this->suggerimentiRubrica = $this->searchRubrica((string) $this->ricercaChiamante); } public function salvaPostIt(): void { if (! $this->isPostItTableReady()) { Notification::make() ->title('Tabella chiamate non pronta') ->body('Esegui prima le migrazioni per usare il nuovo Post-it chiamate.') ->danger() ->send(); return; } $this->validate([ 'telefono' => ['nullable', 'string', 'max:32'], 'nomeChiamante' => ['nullable', 'string', 'max:255'], 'direzione' => ['required', 'in:in_arrivo,in_uscita,persa'], 'esito' => ['nullable', 'string', 'max:255'], 'oggetto' => ['required', 'string', 'max:255'], 'nota' => ['required', 'string'], 'priorita' => ['required', 'in:Bassa,Media,Alta,Urgente'], 'rubricaId' => ['nullable', 'integer', 'exists:rubrica_universale,id'], 'stabileId' => ['nullable', 'integer', 'exists:stabili,id'], ]); try { $this->createPostItRecord(); } catch (QueryException $e) { Notification::make() ->title('Tabella chiamate non pronta') ->body('Esegui prima le migrazioni per usare il nuovo Post-it chiamate.') ->danger() ->send(); return; } $this->resetFormState(); Notification::make() ->title('Post-it salvato') ->success() ->send(); } public function salvaEConvertiInTicket(): void { if (! $this->isPostItTableReady()) { $this->creaTicketDirettoDaForm(); return; } $this->validate([ 'telefono' => ['nullable', 'string', 'max:32'], 'nomeChiamante' => ['nullable', 'string', 'max:255'], 'direzione' => ['required', 'in:in_arrivo,in_uscita,persa'], 'esito' => ['nullable', 'string', 'max:255'], 'oggetto' => ['required', 'string', 'max:255'], 'nota' => ['required', 'string'], 'priorita' => ['required', 'in:Bassa,Media,Alta,Urgente'], 'rubricaId' => ['nullable', 'integer', 'exists:rubrica_universale,id'], 'stabileId' => ['required', 'integer', 'exists:stabili,id'], ]); try { $postIt = $this->createPostItRecord(); } catch (QueryException) { Notification::make() ->title('Tabella chiamate non pronta') ->body('Esegui prima le migrazioni per usare il nuovo Post-it chiamate.') ->danger() ->send(); return; } $this->convertiInTicket((int) $postIt->id); $this->resetFormState(); } public function convertiInTicket(int $postItId): void { if (! $this->isPostItTableReady()) { Notification::make() ->title('Tabella chiamate non pronta') ->body('Esegui prima le migrazioni per usare la gestione Post-it.') ->danger() ->send(); return; } $postIt = ChiamataPostIt::query()->find($postItId); if (! $postIt) { return; } if ($postIt->ticket_id) { Notification::make()->title('Gia convertito in ticket')->warning()->send(); return; } if (! $postIt->stabile_id) { Notification::make() ->title('Stabile mancante') ->body('Associa prima uno stabile al Post-it per poter creare il ticket.') ->danger() ->send(); return; } $ticket = Ticket::query()->create([ 'stabile_id' => $postIt->stabile_id, 'aperto_da_user_id' => Auth::id(), 'assegnato_a_user_id' => $postIt->assegnazione_tipo !== 'fornitore' ? $postIt->assegnato_a_user_id : null, 'assegnato_a_fornitore_id' => $postIt->assegnazione_tipo === 'fornitore' ? $postIt->assegnato_a_fornitore_id : null, 'titolo' => $postIt->oggetto ?: ('Chiamata da ' . ($postIt->nome_chiamante ?: 'Contatto sconosciuto')), 'descrizione' => trim(($postIt->nota ?? '') . "\n\nRiferimento chiamata: #{$postIt->id}"), 'data_apertura' => now(), 'stato' => 'Aperto', 'priorita' => $postIt->priorita, ]); $postIt->ticket_id = $ticket->id; $postIt->stato = 'ticket'; $postIt->save(); Notification::make() ->title('Ticket creato') ->body("Ticket #{$ticket->id} creato dal Post-it #{$postIt->id}") ->success() ->send(); } public function chiudiPostIt(int $postItId): void { if (! $this->isPostItTableReady()) { return; } $postIt = ChiamataPostIt::query()->find($postItId); if (! $postIt) { return; } $postIt->stato = 'chiusa'; $postIt->save(); Notification::make() ->title('Post-it chiuso') ->success() ->send(); } public function getRecentiProperty() { if (! $this->isPostItTableReady()) { return collect(); } try { $items = ChiamataPostIt::query() ->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome']) ->orderByDesc('chiamata_il') ->limit(60) ->get(); $grouped = $items ->groupBy(fn(ChiamataPostIt $item): string => $this->buildRecenteGroupKey($item)) ->map(function (Collection $group): array { /** @var ChiamataPostIt $primary */ $primary = $group->first(); $callerLabel = $this->resolveRecenteCallerLabel($group); $phoneLabel = $this->resolveRecentePhoneLabel($group); return [ 'primary' => $primary, 'items' => $group->values(), 'count' => $group->count(), 'caller_label' => $callerLabel, 'phone' => $phoneLabel, 'latest_at' => optional($primary->chiamata_il)->format('d/m/Y H:i'), 'contains_selected' => $this->selectedPostItId ? $group->contains(fn(ChiamataPostIt $item) : bool => (int) $item->id === (int) $this->selectedPostItId) : false, ]; }) ->values(); if (! $this->selectedPostItId && $grouped->isNotEmpty()) { $first = $grouped->first(); $this->selectedPostItId = (int) ($this->focusPostItId ?: ($first['primary']->id ?? 0)); $this->loadSelectedPostItAssignment(); } return $grouped; } catch (QueryException) { return collect(); } } public function getSelectedPostItProperty(): ?ChiamataPostIt { if (! $this->isPostItTableReady() || ! $this->selectedPostItId) { return null; } return ChiamataPostIt::query() ->with(['stabile', 'ticket', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome']) ->find($this->selectedPostItId); } public function getOperatoriOptionsProperty(): array { return User::query() ->role(['super-admin', 'admin', 'amministratore']) ->orderBy('name') ->limit(100) ->pluck('name', 'id') ->map(fn($name): string => (string) $name) ->all(); } public function getCollaboratoriOptionsProperty(): array { $adminId = $this->resolveCurrentAmministratoreId(); return User::query() ->whereHas('roles', function ($q): void { $q->where('name', 'collaboratore'); }) ->when($adminId > 0, function ($query) use ($adminId): void { $query->whereHas('stabiliAssegnati', function ($inner) use ($adminId): void { $inner->where('amministratore_id', $adminId); }); }) ->orderBy('name') ->limit(150) ->pluck('name', 'id') ->map(fn($name): string => (string) $name) ->all(); } public function getFornitoriOptionsProperty(): array { $adminId = $this->resolveCurrentAmministratoreId(); return Fornitore::query() ->when($adminId > 0, fn($query) => $query->where('amministratore_id', $adminId)) ->orderByRaw("COALESCE(ragione_sociale, CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, '')))") ->limit(150) ->get(['id', 'ragione_sociale', 'nome', 'cognome']) ->mapWithKeys(function (Fornitore $fornitore): array { $label = trim((string) ($fornitore->ragione_sociale ?: (($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? '')))); return [(int) $fornitore->id => ($label !== '' ? $label : ('Fornitore #' . $fornitore->id))]; }) ->all(); } private function creaTicketDirettoDaForm(): void { $this->validate([ 'telefono' => ['nullable', 'string', 'max:32'], 'nomeChiamante' => ['nullable', 'string', 'max:255'], 'oggetto' => ['required', 'string', 'max:255'], 'nota' => ['required', 'string'], 'priorita' => ['required', 'in:Bassa,Media,Alta,Urgente'], 'stabileId' => ['required', 'integer', 'exists:stabili,id'], ]); $ticket = Ticket::query()->create([ 'stabile_id' => $this->stabileId, 'aperto_da_user_id' => Auth::id(), 'titolo' => $this->oggetto ?: ('Chiamata da ' . ($this->nomeChiamante ?: 'Contatto sconosciuto')), 'descrizione' => trim(($this->nota ?? '') . "\n\nOrigine: Ticket diretto da Post-it (tabella chiamate non disponibile)"), 'data_apertura' => now(), 'stato' => 'Aperto', 'priorita' => $this->priorita, ]); Notification::make() ->title('Ticket creato senza tabella Post-it') ->body('Ticket #' . $ticket->id . ' creato. Appena pronta la tabella chiamate, potrai usare la gestione Post-it completa.') ->warning() ->send(); $this->resetFormState(); } private function findRubricaByPhone(string $value): ?RubricaUniversale { return app(RubricaPhoneLookupService::class)->findByPhone($value); } private function createPostItRecord(): ChiamataPostIt { return ChiamataPostIt::query()->create([ 'telefono' => $this->telefono ? PhoneNumber::toE164Italy($this->telefono) ?? $this->telefono : null, 'nome_chiamante' => $this->nomeChiamante, 'direzione' => $this->direzione, 'esito' => $this->esito, 'oggetto' => $this->oggetto, 'nota' => $this->nota, 'priorita' => $this->priorita, 'stato' => 'post_it', 'rubrica_id' => $this->rubricaId, 'stabile_id' => $this->stabileId, 'creato_da_user_id' => Auth::id(), 'chiamata_il' => now(), ]); } private function applyQueryPrefill(): void { $source = trim((string) request()->query('source', '')); $this->disableAutoStabileAssociation = request()->boolean('lock_stabile') || $source === 'topbar_live_call'; $telefono = trim((string) request()->query('telefono', '')); $nome = trim((string) request()->query('nome', '')); $nota = trim((string) request()->query('nota', '')); $oggetto = trim((string) request()->query('oggetto', '')); $direzione = trim((string) request()->query('direzione', '')); $rubricaId = (int) request()->query('rubrica_id', 0); if ($telefono !== '') { $this->telefono = $telefono; } if ($nome !== '') { $this->nomeChiamante = $nome; $this->ricercaChiamante = $nome; } if ($nota !== '') { $this->nota = $nota; } if ($oggetto !== '') { $this->oggetto = $oggetto; } if (in_array($direzione, ['in_arrivo', 'in_uscita', 'persa'], true)) { $this->direzione = $direzione; } if ($rubricaId > 0) { $rubrica = RubricaUniversale::query()->find($rubricaId); if ($rubrica) { $this->rubricaId = (int) $rubrica->id; if ($this->nomeChiamante === null || trim((string) $this->nomeChiamante) === '') { $this->nomeChiamante = $rubrica->nome_completo ?: $rubrica->ragione_sociale; } } } if ($this->disableAutoStabileAssociation) { $this->stabileId = null; } if ($source === 'topbar_live_call' && $telefono !== '') { $this->hydrateRubricaForPhonePrefill($telefono, $nome); if ($this->nota === null || trim((string) $this->nota) === '') { $this->nota = 'Chiamata in ingresso da ' . $telefono; } if ($this->oggetto === null || trim((string) $this->oggetto) === '') { $this->oggetto = 'Chiamata in arrivo da valutare'; } } } private function hydrateRubricaForPhonePrefill(string $phone, string $name = ''): void { $match = $this->findRubricaByPhone($phone); if (! $match && $this->canCreateRubricaDraftFromPhone($phone)) { $match = $this->createRubricaDraftFromPhone($phone, $name); } if (! $match) { return; } $this->rubricaId = (int) $match->id; if ($this->nomeChiamante === null || trim((string) $this->nomeChiamante) === '') { $this->nomeChiamante = $match->nome_completo ?: $match->ragione_sociale; } if ($this->ricercaChiamante === null || trim((string) $this->ricercaChiamante) === '') { $this->ricercaChiamante = $match->nome_completo ?: ($match->ragione_sociale ?: ''); } } private function canCreateRubricaDraftFromPhone(string $phone): bool { $digits = preg_replace('/\D+/', '', $phone) ?: ''; return strlen($digits) >= 6; } private function createRubricaDraftFromPhone(string $phone, string $name = ''): ?RubricaUniversale { $digits = preg_replace('/\D+/', '', $phone) ?: ''; if ($digits === '') { return null; } $normalizedPhone = PhoneNumber::toE164Italy($phone) ?? $digits; $adminId = $this->resolveCurrentAmministratoreId(); $cleanName = trim($name); $payload = [ 'amministratore_id' => $adminId > 0 ? $adminId : null, 'tipo_contatto' => str_contains($cleanName, ' ') ? 'persona_fisica' : 'persona_giuridica', 'telefono_cellulare' => $normalizedPhone, 'categoria' => 'altro', 'stato' => 'attivo', 'data_inserimento' => now()->toDateString(), 'data_ultima_modifica' => now()->toDateString(), 'creato_da' => Auth::id(), 'modificato_da' => Auth::id(), 'note_segreteria' => 'Bozza creata dal box Post-it in topbar per una chiamata in ingresso.', ]; if ($cleanName !== '' && str_contains($cleanName, ' ')) { $parts = preg_split('/\s+/', $cleanName) ?: []; $payload['nome'] = array_shift($parts) ?: null; $payload['cognome'] = $parts !== [] ? implode(' ', $parts) : null; } elseif ($cleanName !== '') { $payload['ragione_sociale'] = $cleanName; } else { $payload['ragione_sociale'] = 'Contatto ' . $digits; } return RubricaUniversale::query()->create($payload); } private function buildRecenteGroupKey(ChiamataPostIt $item): string { $phone = preg_replace('/\D+/', '', (string) ($item->telefono ?? '')) ?: ''; if ($phone !== '') { return 'phone:' . $phone; } if ((int) ($item->rubrica_id ?? 0) > 0) { return 'rubrica:' . (int) $item->rubrica_id; } return 'caller:' . mb_strtolower(trim((string) ($item->nome_chiamante ?? 'sconosciuto'))); } private function resolveRecenteCallerLabel(Collection $group): string { foreach ($group as $item) { if ($item instanceof ChiamataPostIt && $item->rubrica) { return $item->rubrica->nome_completo ?: ($item->rubrica->ragione_sociale ?: 'Contatto'); } } foreach ($group as $item) { $label = trim((string) ($item->nome_chiamante ?? '')); if ($label !== '' && ! $this->isGenericRecenteCallerLabel($label, (string) ($item->telefono ?? ''))) { return $label; } } return 'Contatto non in Rubrica'; } private function resolveRecentePhoneLabel(Collection $group): string { foreach ($group as $item) { $phone = trim((string) ($item->telefono ?? '')); if ($phone !== '') { return $phone; } } return ''; } private function isGenericRecenteCallerLabel(string $label, string $phone = ''): bool { $normalizedLabel = mb_strtolower(trim($label)); if ($normalizedLabel === '' || in_array($normalizedLabel, ['chiamante non identificato', 'contatto non in rubrica'], true)) { return true; } $labelDigits = preg_replace('/\D+/', '', $label) ?: ''; $phoneDigits = preg_replace('/\D+/', '', $phone) ?: ''; return $labelDigits !== '' && $phoneDigits !== '' && $labelDigits === $phoneDigits; } private function loadSelectedPostItAssignment(): void { $postIt = $this->selectedPostIt; if (! $postIt) { return; } $this->assegnazioneTipo = (string) ($postIt->assegnazione_tipo ?: 'operatore'); $this->assegnazioneUserId = (int) ($postIt->assegnato_a_user_id ?? 0) ?: null; $this->assegnazioneFornitoreId = (int) ($postIt->assegnato_a_fornitore_id ?? 0) ?: null; } private function getSelectedAssignmentLabel(): string { return match ($this->assegnazioneTipo) { 'fornitore' => 'fornitore #' . (int) ($this->assegnazioneFornitoreId ?? 0), 'collaboratore' => 'collaboratore #' . (int) ($this->assegnazioneUserId ?? 0), default => 'operatore #' . (int) ($this->assegnazioneUserId ?? 0), }; } private function resolveCurrentAmministratoreId(): int { $user = Auth::user(); if (! $user instanceof User) { return 0; } if ($user->amministratore) { return (int) $user->amministratore->id; } $stabile = StabileContext::getActiveStabile($user); return (int) ($stabile?->amministratore_id ?? 0); } private function resetFormState(): void { $this->telefono = null; $this->nomeChiamante = null; $this->ricercaChiamante = null; $this->direzione = 'in_arrivo'; $this->esito = null; $this->oggetto = null; $this->nota = null; $this->priorita = 'Media'; $this->rubricaId = null; $this->stabileId = null; $this->selectedSuggerimentoId = null; $this->suggerimentiRubrica = collect(); } /** * @return Collection */ private function searchRubrica(string $value): Collection { $query = trim($value); if ($query === '') { return collect(); } $digits = preg_replace('/\D+/', '', $query) ?: ''; $needleText = '%' . mb_strtolower($query) . '%'; $needleDigits = '%' . $digits . '%'; return RubricaUniversale::query() ->where(function ($q) use ($needleText, $needleDigits, $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 ?", [$needleDigits]) ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needleDigits]) ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", [$needleDigits]); } }) ->limit(8) ->get(); } public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } }