From 236aef53cd8fe662f101e36baaab963d69098b52 Mon Sep 17 00:00:00 2001 From: michele Date: Sat, 4 Apr 2026 13:13:05 +0000 Subject: [PATCH] feat: consolida post-it pbx e continuita operativa --- CHANGELOG.md | 4 + .../Commands/NetgesconGitSyncCommand.php | 10 + .../Impostazioni/SchedaAmministratore.php | 152 ++++++++++++- app/Filament/Pages/Strumenti/PostIt.php | 203 +++++++++++++++++- .../Pages/Strumenti/PostItGestione.php | 98 ++++++++- app/Filament/Pages/Supporto/Modifiche.php | 12 ++ app/Models/ChiamataPostIt.php | 13 ++ ...nment_fields_to_chiamate_post_it_table.php | 62 ++++++ docs/support/AGGIORNAMENTI-STAGING-GIT.md | 15 +- .../ARCHITETTURA-RUOLI-MODULI-COWORKING.md | 145 +++++++++++++ docs/support/CONTINUITA-SVILUPPO-AGENT.md | 16 ++ docs/support/CRM-ANAGRAFICA-LEGAMI.md | 4 + docs/support/DISTRIBUZIONE-SERVER-DEBUG.md | 107 +++++++++ docs/support/MANUALE-SVILUPPO-OPERATIVO.md | 30 +++ .../scheda-amministratore-pbx-tapi.blade.php | 95 ++++++++ .../strumenti/post-it-gestione.blade.php | 54 +++++ .../pages/strumenti/post-it.blade.php | 126 +++++++++++ 17 files changed, 1139 insertions(+), 7 deletions(-) create mode 100644 database/migrations/2026_03_15_091500_add_assignment_fields_to_chiamate_post_it_table.php create mode 100644 docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md create mode 100644 docs/support/DISTRIBUZIONE-SERVER-DEBUG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e1118..5a374fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ ## [Unreleased] - Added live-call CRM improvements in Ticket Mobile and Post-it Gestione: cleaner call context, operator note before conversion, clearer SMDR/CSTA labels, and line/group filtering. - Extended PBX day0 preset coverage to include main/admin lines `0001` and `0003`, with aligned studio watchlist for broader staging monitoring. - Documented current CTI staging status and Windows restart procedure so the deployment trail remains in-repo even if the active chat is interrupted. +- Added grouped Post-it call boxes, direct Post-it opening from grouped history, and explicit Post-it assignment to operatore, fornitore, or collaboratore. +- Extended Scheda Amministratore PBX/TAPI with observed tokens, multi-line collaborator mapping, and inline PBX flag editing. +- Included safe rubrica-link QA repair in the Git-first staging sync flow from Supporto > Modifiche. +- Documented the modular coworking direction of NetGescon and the first server-distribution path for isolated debug nodes. ## [0.1.0] - 2026-01-17 diff --git a/app/Console/Commands/NetgesconGitSyncCommand.php b/app/Console/Commands/NetgesconGitSyncCommand.php index 60e0406..f102634 100644 --- a/app/Console/Commands/NetgesconGitSyncCommand.php +++ b/app/Console/Commands/NetgesconGitSyncCommand.php @@ -290,6 +290,15 @@ public function handle(): int return self::FAILURE; } + $this->writeProgress(80, 'Ricucitura legami rubrica CRM', 'running'); + $rubricaQaExit = 0; + try { + $rubricaQaExit = Artisan::call('gescon:qa-rubrica-legami', ['--apply-safe' => true]); + } catch (\Throwable $e) { + $rubricaQaExit = 1; + $this->warn('Ricucitura legami rubrica non completata: ' . $e->getMessage()); + } + $this->writeProgress(82, 'Pulizia cache applicativa', 'running'); $this->callSilently('optimize:clear'); @@ -312,6 +321,7 @@ public function handle(): int 'repo_path' => $repoPath, 'deploy_path' => $deployPath, 'mode' => $deploySync ? 'external-repo-rsync' : 'in-place-git', + 'rubrica_legami_qa' => $rubricaQaExit === 0 ? 'completed' : 'warning', ]); $this->info('Sync Git completata con successo.'); diff --git a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php index 62df6e1..95ad311 100644 --- a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php +++ b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php @@ -64,6 +64,21 @@ class SchedaAmministratore extends Page implements HasForms /** @var array */ public array $collaboratorePbxExtension = []; + /** @var array */ + public array $collaboratorePbxExtensions = []; + + /** @var array */ + public array $collaboratorePbxGroups = []; + + /** @var array */ + public array $collaboratorePbxLines = []; + + /** @var array */ + public array $collaboratorePbxPopupEnabled = []; + + /** @var array */ + public array $collaboratorePbxClickToCallEnabled = []; + public Amministratore $amministratore; public static function canAccess(): bool @@ -1284,6 +1299,8 @@ public function getAccessoAmministratoreRowsProperty(): array */ public function getCollaboratoriCentralinoRowsProperty(): array { + $mappings = (array) Arr::get($this->amministratore->impostazioni ?? [], 'pbx.user_mappings', []); + $query = User::query() ->with('roles') ->whereHas('roles', function ($q): void { @@ -1298,7 +1315,21 @@ public function getCollaboratoriCentralinoRowsProperty(): array return $query ->get() ->map(function (User $user): array { + $mapping = (array) ($mappings[(string) $user->id] ?? []); + $extensions = array_values(array_filter((array) ($mapping['extensions'] ?? []), fn($value): bool => is_string($value) && trim($value) !== '')); + $groups = array_values(array_filter((array) ($mapping['groups'] ?? []), fn($value): bool => is_string($value) && trim($value) !== '')); + $lines = array_values(array_filter((array) ($mapping['lines'] ?? []), fn($value): bool => is_string($value) && trim($value) !== '')); + + if ($extensions === [] && filled($user->pbx_extension)) { + $extensions = [(string) $user->pbx_extension]; + } + $this->collaboratorePbxExtension[(int) $user->id] = (string) ($user->pbx_extension ?? ''); + $this->collaboratorePbxExtensions[(int) $user->id] = implode(', ', $extensions); + $this->collaboratorePbxGroups[(int) $user->id] = implode(', ', $groups); + $this->collaboratorePbxLines[(int) $user->id] = implode(', ', $lines); + $this->collaboratorePbxPopupEnabled[(int) $user->id] = (bool) ($user->pbx_popup_enabled ?? false); + $this->collaboratorePbxClickToCallEnabled[(int) $user->id] = (bool) ($user->pbx_click_to_call_enabled ?? false); return [ 'user_id' => (int) $user->id, @@ -1306,6 +1337,9 @@ public function getCollaboratoriCentralinoRowsProperty(): array 'email' => (string) $user->email, 'ruoli' => $user->roles->pluck('name')->values()->all(), 'pbx_extension' => (string) ($user->pbx_extension ?? ''), + 'pbx_extensions' => $this->collaboratorePbxExtensions[(int) $user->id], + 'pbx_groups' => $this->collaboratorePbxGroups[(int) $user->id], + 'pbx_lines' => $this->collaboratorePbxLines[(int) $user->id], 'pbx_popup_enabled' => (bool) ($user->pbx_popup_enabled ?? false), 'pbx_click_to_call_enabled' => (bool) ($user->pbx_click_to_call_enabled ?? false), ]; @@ -1313,6 +1347,56 @@ public function getCollaboratoriCentralinoRowsProperty(): array ->all(); } + /** + * @return array{extensions:array,groups:array,lines:array} + */ + public function getPbxObservedTokensProperty(): array + { + if (! Schema::hasTable('communication_messages')) { + return [ + 'extensions' => [], + 'groups' => [], + 'lines' => [], + ]; + } + + $rows = $this->pbxRawMessagesQuery() + ->latest('id') + ->limit(300) + ->get(['target_extension', 'message_text', 'metadata']); + + $extensions = []; + $groups = []; + $lines = []; + + foreach ($rows as $message) { + $metadata = (array) ($message->metadata ?? []); + + $this->collectPbxTokens($extensions, [ + (string) ($message->target_extension ?? ''), + (string) data_get($metadata, 'called_extension', ''), + (string) data_get($metadata, 'source_extension', ''), + ], '/\b(?:EXT\d+|\d{2,5})\b/i'); + + $this->collectPbxTokens($groups, [ + (string) data_get($metadata, 'message_text', ''), + (string) ($message->message_text ?? ''), + ], '/\bGRP\d+\b/i'); + + $this->collectPbxTokens($lines, [ + (string) data_get($metadata, 'smdr.co', ''), + (string) data_get($metadata, 'line', ''), + (string) data_get($metadata, 'did', ''), + ], '/\b(?:\d{3,6}|EXT\d+|GRP\d+)\b/i'); + } + + return [ + 'extensions' => array_slice(array_values(array_unique($extensions)), 0, 24), + 'groups' => array_slice(array_values(array_unique($groups)), 0, 24), + 'lines' => array_slice(array_values(array_unique($lines)), 0, 24), + ]; + } + /** * @return array{status:string,label:string,provider:string,channel:string,bridge_mode:string,watched_extensions:array,response_groups:array>,incoming_lines:array>,last_event_at:?string,last_event_type:?string,total_messages:int,last_24h:int,missed_calls:int,assessment:string} */ @@ -1577,16 +1661,78 @@ public function salvaInternoCollaboratore(int $userId): void return; } - $extension = trim((string) ($this->collaboratorePbxExtension[$userId] ?? '')); - $user->pbx_extension = $extension !== '' ? $extension : null; + $extensions = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxExtensions[$userId] ?? $this->collaboratorePbxExtension[$userId] ?? '')); + $groups = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxGroups[$userId] ?? '')); + $lines = $this->normalizePbxMappingInput((string) ($this->collaboratorePbxLines[$userId] ?? '')); + + $user->pbx_extension = $extensions[0] ?? null; + $user->pbx_popup_enabled = (bool) ($this->collaboratorePbxPopupEnabled[$userId] ?? false); + $user->pbx_click_to_call_enabled = (bool) ($this->collaboratorePbxClickToCallEnabled[$userId] ?? false); $user->save(); + $impostazioni = $this->amministratore->impostazioni ?? []; + $pbx = (array) Arr::get($impostazioni, 'pbx', []); + $mappings = (array) ($pbx['user_mappings'] ?? []); + + $mappings[(string) $userId] = [ + 'extensions' => $extensions, + 'groups' => $groups, + 'lines' => $lines, + ]; + + $pbx['user_mappings'] = $mappings; + $impostazioni['pbx'] = $pbx; + $this->amministratore->impostazioni = $impostazioni; + $this->amministratore->save(); + Notification::make() - ->title('Interno collaboratore aggiornato') + ->title('Mappatura PBX collaboratore aggiornata') ->success() ->send(); } + /** + * @param array $target + * @param array $values + */ + private function collectPbxTokens(array &$target, array $values, string $pattern): void + { + foreach ($values as $value) { + if ($value === '') { + continue; + } + + if (preg_match_all($pattern, strtoupper($value), $matches) > 0) { + foreach ((array) ($matches[0] ?? []) as $match) { + $token = trim((string) $match); + if ($token !== '') { + $target[] = $token; + } + } + + continue; + } + + $token = trim(strtoupper($value)); + if ($token !== '') { + $target[] = $token; + } + } + } + + /** + * @return array + */ + private function normalizePbxMappingInput(string $value): array + { + $tokens = preg_split('/[\s,;]+/', strtoupper(trim($value)), -1, PREG_SPLIT_NO_EMPTY) ?: []; + + return array_values(array_unique(array_filter(array_map(function ($token): string { + $clean = preg_replace('/[^A-Z0-9_-]/', '', (string) $token) ?? ''; + return trim($clean); + }, $tokens), fn(string $token): bool => $token !== ''))); + } + public function abilitaAccessoFornitore(int $dipendenteId): void { $dipendente = FornitoreDipendente::query() diff --git a/app/Filament/Pages/Strumenti/PostIt.php b/app/Filament/Pages/Strumenti/PostIt.php index c415f9f..a521a24 100644 --- a/app/Filament/Pages/Strumenti/PostIt.php +++ b/app/Filament/Pages/Strumenti/PostIt.php @@ -2,11 +2,13 @@ namespace App\Filament\Pages\Strumenti; use App\Models\ChiamataPostIt; +use App\Models\Fornitore; use App\Models\RubricaUniversale; use App\Models\Stabile; use App\Models\Ticket; use App\Models\User; use App\Support\PhoneNumber; +use App\Support\StabileContext; use BackedEnum; use Filament\Notifications\Notification; use Filament\Pages\Page; @@ -42,6 +44,11 @@ class PostIt extends Page public string $priorita = 'Media'; public ?int $rubricaId = null; public ?int $stabileId = null; + public ?int $focusPostItId = null; + public ?int $selectedPostItId = null; + public string $assegnazioneTipo = 'operatore'; + public ?int $assegnazioneUserId = null; + public ?int $assegnazioneFornitoreId = null; public ?int $selectedSuggerimentoId = null; @@ -53,6 +60,10 @@ 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; } public function getGestionePostItUrl(): string @@ -65,11 +76,95 @@ 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); @@ -247,6 +342,8 @@ public function convertiInTicket(int $postItId): void $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(), @@ -292,16 +389,81 @@ public function getRecentiProperty() } try { - return ChiamataPostIt::query() - ->with(['rubrica', 'stabile', 'ticket', 'creatoDa']) + $items = ChiamataPostIt::query() + ->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome']) ->orderByDesc('chiamata_il') ->limit(25) ->get(); + + if (! $this->selectedPostItId && $items->isNotEmpty()) { + $this->selectedPostItId = (int) ($this->focusPostItId ?: $items->first()->id); + $this->loadSelectedPostItAssignment(); + } + + return $items; } 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([ @@ -369,6 +531,43 @@ private function createPostItRecord(): ChiamataPostIt ]); } + 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; diff --git a/app/Filament/Pages/Strumenti/PostItGestione.php b/app/Filament/Pages/Strumenti/PostItGestione.php index 1e58549..7f78e6d 100644 --- a/app/Filament/Pages/Strumenti/PostItGestione.php +++ b/app/Filament/Pages/Strumenti/PostItGestione.php @@ -60,6 +60,9 @@ class PostItGestione extends Page /** @var array */ private array $pbxExtensionCache = []; + /** @var array */ + private array $postItMessageCache = []; + public function mount(): void { $focus = (int) request()->query('focus_post_it', 0); @@ -75,6 +78,11 @@ public function getPostItInserimentoUrl(): string return PostIt::getUrl(panel: 'admin-filament'); } + public function getPostItPageUrl(int $postItId): string + { + return PostIt::getUrl(panel: 'admin-filament') . '?focus_post_it=' . $postItId; + } + public function convertiInTicket(int $postItId): void { if (! $this->isPostItTableReady()) { @@ -216,7 +224,7 @@ public function getRecentiProperty() try { $query = ChiamataPostIt::query() - ->with(['rubrica', 'stabile', 'ticket', 'creatoDa']) + ->with(['rubrica', 'stabile', 'ticket', 'creatoDa', 'assegnatoAUser:id,name', 'assegnatoAFornitore:id,ragione_sociale,nome,cognome']) ->orderByDesc('chiamata_il') ->orderByDesc('id'); @@ -230,6 +238,41 @@ public function getRecentiProperty() } } + public function getRecentiRaggruppatiProperty() + { + $groups = []; + + foreach ($this->recenti as $postIt) { + if ($this->isInternalPostIt($postIt)) { + continue; + } + + $groupKey = $this->buildGroupedPostItKey($postIt); + $lastIndex = count($groups) - 1; + + if ($lastIndex >= 0 && $groups[$lastIndex]['key'] === $groupKey) { + $groups[$lastIndex]['items'][] = $postIt; + $groups[$lastIndex]['count']++; + $groups[$lastIndex]['last_at'] = optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-'; + continue; + } + + $groups[] = [ + 'key' => $groupKey, + 'items' => [$postIt], + 'count' => 1, + 'latest' => $postIt, + 'caller_label' => $postIt->nome_chiamante ?: 'Chiamante non identificato', + 'phone' => (string) ($postIt->telefono ?? ''), + 'route_label' => $this->resolvePostItRouteLabel($postIt), + 'first_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-', + 'last_at' => optional($postIt->chiamata_il)->format('d/m/Y H:i') ?: '-', + ]; + } + + return collect($groups); + } + public function getChiamateTecnicheProperty() { if (! Schema::hasTable('communication_messages')) { @@ -518,6 +561,59 @@ private function buildTicketDescriptionFromPostIt(int $postItId, ChiamataPostIt return implode("\n\n", $parts); } + private function buildGroupedPostItKey(ChiamataPostIt $postIt): string + { + $phone = preg_replace('/\D+/', '', (string) ($postIt->telefono ?? '')) ?: ''; + $caller = mb_strtolower(trim((string) ($postIt->nome_chiamante ?? ''))); + $route = mb_strtolower($this->resolvePostItRouteLabel($postIt)); + + return implode('|', [$phone, $caller, $route, (string) $postIt->direzione]); + } + + private function resolvePostItRouteLabel(ChiamataPostIt $postIt): string + { + $message = $this->resolveMessageFromPostIt($postIt); + if ($message instanceof CommunicationMessage) { + $line = trim((string) ($message->target_extension ?: data_get($message->metadata, 'smdr.co', ''))); + if ($line !== '') { + return $line; + } + } + + if (preg_match('/linea\s+([^\n]+)/i', (string) ($postIt->oggetto ?? ''), $matches) === 1) { + return trim((string) $matches[1]); + } + + return '-'; + } + + private function isInternalPostIt(ChiamataPostIt $postIt): bool + { + $message = $this->resolveMessageFromPostIt($postIt); + if ($message instanceof CommunicationMessage) { + return $this->isInternalSmdrMessage($message); + } + + return false; + } + + private function resolveMessageFromPostIt(ChiamataPostIt $postIt): ?CommunicationMessage + { + $originId = (int) ($postIt->origine_id ?? 0); + if ($originId <= 0) { + return null; + } + + if (array_key_exists($originId, $this->postItMessageCache)) { + return $this->postItMessageCache[$originId]; + } + + $message = CommunicationMessage::query()->find($originId); + $this->postItMessageCache[$originId] = $message; + + return $message; + } + public function richiediClickToCallDaMessaggio(int $messageId): void { $user = Auth::user(); diff --git a/app/Filament/Pages/Supporto/Modifiche.php b/app/Filament/Pages/Supporto/Modifiche.php index de9fde8..c45dd91 100644 --- a/app/Filament/Pages/Supporto/Modifiche.php +++ b/app/Filament/Pages/Supporto/Modifiche.php @@ -1096,6 +1096,18 @@ private function loadManualSections(): void 'description' => 'Regole pratiche per aggiornare documentazione, note commit e materiali utili al riaggancio delle prossime sessioni.', 'path' => 'docs/support/CONTINUITA-SVILUPPO-AGENT.md', ], + [ + 'key' => 'coworking-moduli', + 'title' => 'Architettura ruoli, moduli e coworking', + 'description' => 'Direzione architetturale di NetGescon come spazio unico abilitato per ruoli multipli e moduli attivabili sulla stessa base dominio.', + 'path' => 'docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md', + ], + [ + 'key' => 'server-debug', + 'title' => 'Distribuzione server e debug separato', + 'description' => 'Primo piano pratico per portare NetGescon su nodi separati dalla rete locale e stabilizzare deploy, collaudo e diagnostica.', + 'path' => 'docs/support/DISTRIBUZIONE-SERVER-DEBUG.md', + ], ]; foreach ($documents as $document) { diff --git a/app/Models/ChiamataPostIt.php b/app/Models/ChiamataPostIt.php index d82b7e0..2763b08 100644 --- a/app/Models/ChiamataPostIt.php +++ b/app/Models/ChiamataPostIt.php @@ -15,6 +15,9 @@ class ChiamataPostIt extends Model 'rubrica_id', 'ticket_id', 'creato_da_user_id', + 'assegnato_a_user_id', + 'assegnato_a_fornitore_id', + 'assegnazione_tipo', 'telefono', 'nome_chiamante', 'direzione', @@ -57,4 +60,14 @@ public function creatoDa() { return $this->belongsTo(User::class, 'creato_da_user_id'); } + + public function assegnatoAUser() + { + return $this->belongsTo(User::class, 'assegnato_a_user_id'); + } + + public function assegnatoAFornitore() + { + return $this->belongsTo(Fornitore::class, 'assegnato_a_fornitore_id'); + } } diff --git a/database/migrations/2026_03_15_091500_add_assignment_fields_to_chiamate_post_it_table.php b/database/migrations/2026_03_15_091500_add_assignment_fields_to_chiamate_post_it_table.php new file mode 100644 index 0000000..773d64e --- /dev/null +++ b/database/migrations/2026_03_15_091500_add_assignment_fields_to_chiamate_post_it_table.php @@ -0,0 +1,62 @@ +foreignId('assegnato_a_user_id')->nullable()->after('creato_da_user_id')->constrained('users')->nullOnDelete(); + } + + if (! Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id')) { + $table->foreignId('assegnato_a_fornitore_id')->nullable()->after('assegnato_a_user_id')->constrained('fornitori')->nullOnDelete(); + } + + if (! Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')) { + $table->string('assegnazione_tipo', 32)->nullable()->after('assegnato_a_fornitore_id'); + $table->index(['assegnazione_tipo', 'assegnato_a_user_id'], 'chiamate_post_it_assign_user_idx'); + $table->index(['assegnazione_tipo', 'assegnato_a_fornitore_id'], 'chiamate_post_it_assign_supplier_idx'); + } + }); + } + + public function down(): void + { + Schema::table('chiamate_post_it', function (Blueprint $table): void { + if (Schema::hasColumn('chiamate_post_it', 'assegnazione_tipo')) { + try { + $table->dropIndex('chiamate_post_it_assign_user_idx'); + } catch (Throwable) { + } + + try { + $table->dropIndex('chiamate_post_it_assign_supplier_idx'); + } catch (Throwable) { + } + + $table->dropColumn('assegnazione_tipo'); + } + + if (Schema::hasColumn('chiamate_post_it', 'assegnato_a_fornitore_id')) { + try { + $table->dropConstrainedForeignId('assegnato_a_fornitore_id'); + } catch (Throwable) { + $table->dropColumn('assegnato_a_fornitore_id'); + } + } + + if (Schema::hasColumn('chiamate_post_it', 'assegnato_a_user_id')) { + try { + $table->dropConstrainedForeignId('assegnato_a_user_id'); + } catch (Throwable) { + $table->dropColumn('assegnato_a_user_id'); + } + } + }); + } +}; \ No newline at end of file diff --git a/docs/support/AGGIORNAMENTI-STAGING-GIT.md b/docs/support/AGGIORNAMENTI-STAGING-GIT.md index 94e114e..4d49c0c 100644 --- a/docs/support/AGGIORNAMENTI-STAGING-GIT.md +++ b/docs/support/AGGIORNAMENTI-STAGING-GIT.md @@ -31,10 +31,23 @@ ## UI e comando predisposto 1. fa fetch dal repository Gitea 2. allinea il nodo al branch richiesto 3. rigenera il changelog Git automatico per la pagina supporto -4. salva esito e diagnostica in `storage/app/support/generated/git-sync-last.json` +4. lancia anche la ricucitura sicura dei legami CRM rubrica se il comando QA e disponibile +5. salva esito e diagnostica in `storage/app/support/generated/git-sync-last.json` Quindi la UI non e un percorso separato: e il front-end controllato del flusso Git ufficiale. +## Ricucitura CRM inclusa nella UI + +La procedura `Supporto > Modifiche` non richiede piu che l operatore esegua a mano il comando QA rubrica da terminale. + +Durante il sync Git, il backend prova anche a eseguire: + +```bash +php artisan gescon:qa-rubrica-legami --apply-safe +``` + +In questo modo la riallocazione dei legami sicuri tra rubrica, stabile e unita rientra nel normale aggiornamento UI di staging. + ## Cosa stiamo aggiornando in questo momento ### 1. Supporto > Modifiche come pannello operativo reale diff --git a/docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md b/docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md new file mode 100644 index 0000000..20ae98d --- /dev/null +++ b/docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md @@ -0,0 +1,145 @@ +# Architettura ruoli, moduli e coworking + +## Obiettivo + +NetGescon non va piu letto come un applicativo monoruolo dedicato solo all amministrazione condominiale. + +La direzione corretta e questa: + +1. una base dominio unica e coerente +2. moduli attivabili sopra la stessa base dati +3. ruoli e abilitazioni che decidono cosa una persona puo fare +4. stessa persona abilitabile su piu funzioni operative + +In pratica: uno spazio unico di coworking operativo, non una somma di mini-app scollegate. + +## Base dominio da tenere stabile + +La base comune resta: + +- anagrafica unica dei nominativi +- stabili, unita immobiliari, relazioni e ruoli +- movimenti, banca, ripartizione spese, piano dei conti +- contabilita in partita doppia +- ticket, allegati, documenti e storico operativo + +Questa base non va duplicata per mestiere o per modulo. + +## Cosa cambia davvero tra un uso e l altro + +Quello che cambia non e il dato di partenza, ma: + +- il ruolo +- la procedura disponibile +- il modulo abilitato +- il livello di permesso sul tenant, sullo stabile o sul progetto + +Esempi reali della stessa persona: + +- supporto informatico +- sviluppo software +- revisione contabile +- collaboratore dell amministratore +- gestione PBX / CTI +- supervisione di ticket e fornitori + +## Regola architetturale + +NetGescon deve diventare: + +- domain-first +- permission-first +- module-first + +Non deve diventare: + +- menu-first +- pagina-first +- ruolo singolo hard-coded + +## Moduli principali gia visibili o in formazione + +- Condominio e anagrafica +- Contabilita e banca +- Ripartizioni e tabelle millesimali +- Ticket e Post-it operativi +- CTI / PBX / click-to-call +- Import legacy Gescon +- Revisione contabile +- Storage tenant-aware e archivi +- Distribuzione server e manutenzione nodo + +## Effetto pratico sulla UI + +La UI deve progressivamente convergere verso: + +1. stessa base record +2. azioni visibili solo se abilitate +3. moduli attivabili senza biforcare il prodotto +4. procedure specialistiche innestate sul contesto corretto + +Esempio: + +- una chiamata entra nel CRM +- produce un Post-it +- il Post-it puo essere assegnato a operatore, collaboratore o fornitore +- se serve diventa ticket +- il ticket impatta documenti, fornitore, stabile, contabilita o revisione + +Sempre sullo stesso grafo dati. + +## Modello di abilitazione consigliato + +Tre livelli distinti: + +### 1. Ruolo base + +Definisce il perimetro generale: + +- amministratore +- collaboratore +- fornitore +- revisore +- tecnico +- sviluppatore / ops + +### 2. Capacita operative + +Definiscono le azioni concrete: + +- vedere contabilità +- modificare ripartizioni +- usare CTI +- lanciare sync staging +- aprire ticket +- assegnare Post-it +- gestire fornitori + +### 3. Ambito + +Definisce dove valgono: + +- tenant / amministratore +- stabile +- progetto +- modulo + +## Conseguenza tecnica + +Ogni nuova funzione dovrebbe chiedersi prima: + +1. su quale base dominio si appoggia +2. quale modulo la ospita +3. quale permission la abilita +4. su quale ambito agisce + +Se una funzione non risponde a queste quattro domande, rischia di diventare una pagina isolata e fragile. + +## Decisione attuale + +La direzione da seguire nelle prossime sessioni e: + +1. consolidare i moduli gia emersi invece di moltiplicare pagine parallele +2. portare la gestione operativa sempre piu vicino alle procedure abilitate +3. usare la pagina `Supporto > Modifiche` come ponte fra sviluppo, documentazione e deploy +4. preparare la distribuzione di NetGescon su nodi separati per ambienti di debug e collaudo diff --git a/docs/support/CONTINUITA-SVILUPPO-AGENT.md b/docs/support/CONTINUITA-SVILUPPO-AGENT.md index 3ec10c2..28b11bb 100644 --- a/docs/support/CONTINUITA-SVILUPPO-AGENT.md +++ b/docs/support/CONTINUITA-SVILUPPO-AGENT.md @@ -65,3 +65,19 @@ ## Direzione architetturale attuale - staging deve potersi riallineare da interfaccia web senza richiedere terminale all operatore - CTI/PBX va tenuto aperto a provider diversi, senza chiudersi su Panasonic - le pagine gestionali devono seguire il contesto condiviso topbar per stabile e anno + +## Stato utile da ricordare dopo questo slice + +- `Supporto > Modifiche` e il punto da cui l operatore deve poter riallineare staging e far partire anche la ricucitura rubrica sicura. +- `Strumenti > Post-it Gestione` mostra ora un livello operativo raggruppato per richiami consecutivi, non solo righe singole. +- `Strumenti > Post-it` sta diventando il punto unico dove inserire, prendere in carico e indirizzare il Post-it verso operatore, collaboratore o fornitore. +- `Impostazioni > Scheda Amministratore > PBX / TAPI` e gia il posto giusto per accumulare segnali PBX osservati e mapparli alle persone reali dello studio. +- la direzione architetturale e `coworking unico + moduli abilitabili + ruoli multipli sulla stessa base dominio` +- il prossimo fronte infrastrutturale non e una rivoluzione immediata a microservizi, ma la distribuzione controllata su nodi separati per debug e collaudo fuori rete locale + +## File nuovi o da leggere per ripartire + +- `docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md` +- `docs/support/DISTRIBUZIONE-SERVER-DEBUG.md` +- `docs/support/CRM-ANAGRAFICA-LEGAMI.md` +- `docs/support/AGGIORNAMENTI-STAGING-GIT.md` diff --git a/docs/support/CRM-ANAGRAFICA-LEGAMI.md b/docs/support/CRM-ANAGRAFICA-LEGAMI.md index 833e34b..66abc8b 100644 --- a/docs/support/CRM-ANAGRAFICA-LEGAMI.md +++ b/docs/support/CRM-ANAGRAFICA-LEGAMI.md @@ -21,6 +21,10 @@ ## Ordine corretto della pipeline ## Nuovo comando QA mirato +Per l operatore la via ordinaria non deve essere il terminale: il flusso corretto passa dalla UI `Supporto > Modifiche`, che durante il sync Git prova anche la ricucitura sicura dei legami rubrica. + +Il comando resta comunque disponibile per diagnosi tecniche o ambienti di sviluppo. + Per analizzare casi singoli senza modificare nulla: ```bash diff --git a/docs/support/DISTRIBUZIONE-SERVER-DEBUG.md b/docs/support/DISTRIBUZIONE-SERVER-DEBUG.md new file mode 100644 index 0000000..7299f5e --- /dev/null +++ b/docs/support/DISTRIBUZIONE-SERVER-DEBUG.md @@ -0,0 +1,107 @@ +# Distribuzione server e debug separato + +## Obiettivo + +Portare NetGescon fuori dalla sola rete locale e iniziare a lavorare con nodi separati per: + +- deploy piu stabili +- collaudo fuori macchina sviluppo +- debug su ambienti isolati +- verifica reale di update, restore e sincronizzazioni + +## Punto di partenza + +Oggi il flusso piu stabile e: + +1. sviluppo nel workspace Day0 +2. commit e push su Gitea +3. sync UI da `Supporto > Modifiche` +4. verifica su staging + +Questo flusso va mantenuto anche quando iniziamo a distribuire NetGescon su altri server. + +## Strategia consigliata + +### Fase 1. Nodo staging separato e disciplinato + +Obiettivo: + +- continuare a usare staging come nodo intermedio obbligatorio +- verificare che update, migrazioni e ricuciture dati passino sempre dalla procedura Git-first + +Vincoli: + +- niente modifiche manuali non tracciate sul nodo +- niente deploy paralleli fuori procedura + +### Fase 2. Nodo debug esterno alla rete locale + +Obiettivo: + +- avere una macchina separata dalla rete di sviluppo +- usare dati di test controllati +- validare comportamento applicativo, login, PBX, allegati, storage, update + +Uso consigliato: + +- test deployment +- test restore +- test debug multi-macchina +- verifiche su watcher o bridge che non devono vivere solo sul PC principale + +### Fase 3. Pacchetto installabile ripetibile + +Obiettivo: + +- trasformare NetGescon in un installabile coerente +- poter allestire un nuovo nodo senza ricostruire a mano script, env e procedure + +Minimo indispensabile: + +- checklist server +- script bootstrap +- procedura aggiornamento +- procedura backup e restore +- note di rete e storage + +## Architettura iniziale consigliata + +Per il primo ciclo non serve un parco complesso di macchine. + +Bastano tre ruoli chiari: + +1. workspace autorevole Day0 +2. staging operativo aggiornato da Gitea +3. debug node separato per prove e collaudi fuori rete locale + +## Cosa validare sul debug node + +- installazione applicativa pulita +- migrazioni e bootstrap +- login e permessi ruoli +- documentazione in `Supporto > Modifiche` +- procedura Git sync e pacchetto aggiornamento +- Post-it, ticket e CRM telefonico +- storage tenant-aware e documenti sensibili +- eventuali watcher PBX o bridge esterni + +## Regola pratica per il deploy + +Ogni nodo deve poter essere ricostruito da materiale nel repository. + +Quindi il repo deve contenere almeno: + +- manuale operativo +- flusso update staging +- architettura ruoli/moduli +- note continuita agent +- script di update e di sync gia usati dalla UI + +## Decisione attuale + +Per il prossimo passo concreto: + +1. completare il commit del slice corrente +2. aggiornare staging via Git-first +3. preparare una checklist tecnica per il primo nodo debug esterno +4. poi estrarre un bootstrap server riusabile, senza dipendere dalla macchina locale diff --git a/docs/support/MANUALE-SVILUPPO-OPERATIVO.md b/docs/support/MANUALE-SVILUPPO-OPERATIVO.md index a631d6f..fcdbc74 100644 --- a/docs/support/MANUALE-SVILUPPO-OPERATIVO.md +++ b/docs/support/MANUALE-SVILUPPO-OPERATIVO.md @@ -35,6 +35,34 @@ ## Effetto pratico ## Implementazioni recenti da tenere come riferimento +### 0. Direzione di prodotto: NetGescon come spazio unico a ruoli e moduli + +- La direzione attuale non e piu quella di un gestionale chiuso solo per l amministratore condominiale. +- NetGescon va trattato come uno spazio operativo unico dove la differenza non la fa la macchina o il menu fisso, ma l abilitazione concessa a ruolo, persona o tenant. +- Lo stesso utente puo lavorare su piu assi: assistenza informatica, sviluppo software, revisione contabile, collaboratore amministrativo, gestione PBX, supporto fornitori. +- La base dominio resta stabile: unita immobiliari, anagrafica unica, spese, banca, piano dei conti, ripartizioni, partita doppia. +- Sopra questa base si attivano moduli o funzioni: CTI/PBX, ticketing, revisione, import legacy, storage tenant-aware, distribuzione server, fornitori, allegati, analytics. + +Effetto pratico: + +- [P] l evoluzione corretta e modulo-first e permission-first, non pagina-first +- [P] lo stesso record dominio deve poter essere usato in procedure diverse senza duplicare dati o UI separate per mestiere +- [U] una persona puo lavorare nello stesso ambiente con ruoli multipli senza cambiare prodotto + +### 0-bis. Slice appena chiuso su CRM telefonico e instradamento umano + +- In `strumenti/post-it-gestione` le chiamate recenti hanno ora anche box raggruppati per chiamante/numero/linea consecutivi. +- Le chiamate interne sono escluse dal raggruppamento operativo, cosi la vista serve davvero per il CRM e non per il rumore tecnico. +- Ogni box puo aprire direttamente il Post-it corrispondente. +- In `strumenti/post-it` il Post-it puo essere assegnato esplicitamente a operatore, collaboratore o fornitore. +- In `impostazioni/scheda-amministratore` il tab PBX/TAPI accumula i token osservati e permette di associare piu linee, gruppi ed extension allo stesso collaboratore. + +Effetto pratico: + +- [U] meno duplicazione nei richiami ripetuti +- [U] piu facile decidere chi prende in carico una chiamata o un ticket derivato +- [P] il PBX viene trattato come sorgente di segnali osservati, non come struttura rigida hard-coded sul fornitore attuale + ### 1. CTI / CSTA / click-to-call piu aperti lato PBX - Separato il tab tecnico SMDR dal tab `CTI / CSTA e Click-to-Call` in `strumenti/post-it-gestione`. @@ -96,3 +124,5 @@ ## Prossimi passi consigliati 1. usare questa sezione come fonte primaria per le prossime implementazioni CTI e contabilita 2. automatizzare anche la scrittura di snapshot di sviluppo basati sui commit e sulle note operative 3. aggiungere progressivamente how-to piu mirati, senza spargere file duplicati +4. spostare le prossime decisioni su ruoli/moduli nel documento architetturale dedicato `docs/support/ARCHITETTURA-RUOLI-MODULI-COWORKING.md` +5. preparare il primo nodo esterno di debug/deploy seguendo `docs/support/DISTRIBUZIONE-SERVER-DEBUG.md` diff --git a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-pbx-tapi.blade.php b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-pbx-tapi.blade.php index b6e3c6c..52dea54 100644 --- a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-pbx-tapi.blade.php +++ b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-pbx-tapi.blade.php @@ -1,4 +1,5 @@ @php($summary = $this->pbxSummary) +@php($observed = $this->pbxObservedTokens)
@@ -94,6 +95,100 @@
+
+
Token PBX osservati dal CRM
+
Il CRM accumula automaticamente gruppi, interni e linee letti dagli eventi telefonici. Usa questi valori per mappare chi gestisce cosa, senza vincolarti a Panasonic.
+ +
+
+
Interni / extension
+
+ @forelse($observed['extensions'] as $token) + {{ $token }} + @empty + Nessun interno osservato. + @endforelse +
+
+
+
Gruppi
+
+ @forelse($observed['groups'] as $token) + {{ $token }} + @empty + Nessun gruppo osservato. + @endforelse +
+
+
+
Linee / DID
+
+ @forelse($observed['lines'] as $token) + {{ $token }} + @empty + Nessuna linea osservata. + @endforelse +
+
+
+
+ +
+
Chi gestisce interni, linee e gruppi
+
Ogni nominativo puo gestire piu interni, piu linee e piu gruppi. I flag PBX restano modificabili inline con check rapidi.
+ +
+ + + + + + + + + + + + + @forelse($this->collaboratoriCentralinoRows as $row) + + + + + + + + + @empty + + + + @endforelse + +
CollaboratoreExtension gestiteGruppi gestitiLinee / DIDFlagsAzioni
+
{{ $row['name'] }}
+
{{ $row['email'] }}
+
+ + + + + + + + + + Salva +
Nessun collaboratore assegnato agli stabili di questo amministratore.
+
+
+
Ultimi eventi letti dal CRM
Se qui compaiono record, almeno uno dei canali telefonici attivi sta gia portando traffico nel CRM. In modalita hybrid puoi vedere sia eventi Panasonic sia righe SMDR.
diff --git a/resources/views/filament/pages/strumenti/post-it-gestione.blade.php b/resources/views/filament/pages/strumenti/post-it-gestione.blade.php index 7b8c34c..8d9f20a 100644 --- a/resources/views/filament/pages/strumenti/post-it-gestione.blade.php +++ b/resources/views/filament/pages/strumenti/post-it-gestione.blade.php @@ -33,6 +33,60 @@

Storico chiamate recenti

+
+
+
+
Box chiamate raggruppate
+
Le comunicazioni interne non compaiono qui. Le chiamate consecutive dello stesso numero restano nello stesso box finche non interviene un altro chiamante.
+
+
+ +
+ @forelse($this->recentiRaggruppati as $gruppo) + @php($box = $gruppo['latest']) +
+
+
+
{{ $gruppo['caller_label'] }}
+
+ {{ $gruppo['phone'] !== '' ? $gruppo['phone'] : 'Numero non disponibile' }} + @if($gruppo['route_label'] !== '-') + · verso {{ $gruppo['route_label'] }} + @endif +
+
+ {{ $gruppo['count'] }} tentativi +
+ +
{{ $gruppo['last_at'] }}{{ $gruppo['count'] > 1 ? ' · gruppo consecutivo' : '' }}
+
{{ $box->oggetto ?: 'Senza oggetto' }}
+ +
+ {{ $box->stato }} + @if($box->assegnazione_tipo) + {{ ucfirst(str_replace('_', ' ', (string) $box->assegnazione_tipo)) }} + @endif + @if($box->assegnatoAUser) + {{ $box->assegnatoAUser->name }} + @endif + @if($box->assegnatoAFornitore) + {{ $box->assegnatoAFornitore->ragione_sociale ?: trim(($box->assegnatoAFornitore->nome ?? '') . ' ' . ($box->assegnatoAFornitore->cognome ?? '')) }} + @endif +
+ +
+ Apri Post-it + @if($box->ticket_id) + Apri ticket + @endif +
+
+ @empty +
Nessuna chiamata esterna raggruppabile disponibile.
+ @endforelse +
+
+
@forelse($this->recenti as $riga)
Salva e crea ticket
+ + @if($this->isPostItTableReady()) +
+
+
+
+

Post-it recenti

+

Seleziona un Post-it per gestirlo o assegnarlo direttamente.

+
+
+ +
+ @forelse($this->recenti as $riga) + + @empty +
Nessun Post-it recente.
+ @endforelse +
+
+ +
+

Assegnazione Post-it

+ @if(! $this->isPostItAssignmentReady()) +
Assegnazione disponibile dopo l'applicazione della nuova migrazione Post-it nel normale flusso di aggiornamento.
+ @elseif($this->selectedPostIt) +
+
Post-it #{{ (int) $this->selectedPostIt->id }} · {{ $this->selectedPostIt->oggetto ?: 'Senza oggetto' }}
+
+ {{ $this->selectedPostIt->nome_chiamante ?: 'Chiamante non identificato' }} + @if($this->selectedPostIt->telefono) + · {{ $this->selectedPostIt->telefono }} + @endif + @if($this->selectedPostIt->ticket_id) + · Ticket #{{ (int) $this->selectedPostIt->ticket_id }} + @endif +
+
+ +
+ + + @if($assegnazioneTipo === 'operatore') + + @elseif($assegnazioneTipo === 'collaboratore') + + @else + + @endif + +
+ Salva assegnazione + @if($this->selectedPostIt->ticket_id) + Apri ticket collegato + @endif +
+
+ @else +
Seleziona un Post-it recente per assegnarlo.
+ @endif +
+
+ @endif