diff --git a/app/Filament/Pages/Gescon/FornitoreScheda.php b/app/Filament/Pages/Gescon/FornitoreScheda.php index 9984f20..647814b 100644 --- a/app/Filament/Pages/Gescon/FornitoreScheda.php +++ b/app/Filament/Pages/Gescon/FornitoreScheda.php @@ -25,6 +25,7 @@ use App\Services\Consumi\ConsumiAcquaTariffeIngestionService; use App\Services\Documenti\PdfTextExtractionService; use App\Services\FattureElettroniche\FatturaElettronicaProtocolloService; +use App\Support\Livewire\SupportsRubricaLookup; use App\Support\StabileContext; use Filament\Actions\Action; use Filament\Forms\Components\Hidden; @@ -43,6 +44,7 @@ class FornitoreScheda extends Page { use ResolvesOperatoreContext; + use SupportsRubricaLookup; protected static ?string $title = 'Scheda fornitore'; @@ -94,6 +96,13 @@ class FornitoreScheda extends Page public string $nuovoDipendenteTelefono = ''; + public string $dipendenteRubricaSearch = ''; + + public ?int $dipendenteRubricaId = null; + + /** @var array> */ + public array $dipendenteRubricaMatches = []; + public ?string $lastGeneratedPassword = null; public function cleanDisplayValue(?string $value, string $fallback = '-'): string @@ -227,6 +236,83 @@ public function creaDipendenteFornitore(): void Notification::make()->title('Dipendente aggiunto')->success()->send(); } + public function updatedDipendenteRubricaSearch(): void + { + $this->searchDipendenteRubricaMatches(); + } + + public function selezionaDipendenteRubrica(int $rubricaId): void + { + $rubrica = RubricaUniversale::query()->find($rubricaId); + if (! $rubrica instanceof RubricaUniversale) { + return; + } + + $this->dipendenteRubricaId = (int) $rubrica->id; + $this->dipendenteRubricaSearch = $this->formatRubricaLookupLabel($rubrica); + $this->dipendenteRubricaMatches = [$this->mapRubricaLookupRow($rubrica)]; + } + + public function resetDipendenteRubricaSelection(): void + { + $this->dipendenteRubricaId = null; + $this->dipendenteRubricaSearch = ''; + $this->dipendenteRubricaMatches = []; + } + + public function collegaDipendenteDaRubrica(): void + { + $rubricaId = (int) ($this->dipendenteRubricaId ?? 0); + if ($rubricaId <= 0) { + Notification::make()->title('Seleziona un nominativo rubrica')->warning()->send(); + return; + } + + $rubrica = RubricaUniversale::query()->find($rubricaId); + if (! $rubrica instanceof RubricaUniversale) { + Notification::make()->title('Nominativo rubrica non trovato')->danger()->send(); + return; + } + + $email = mb_strtolower(trim((string) ($rubrica->email ?? ''))); + + $query = FornitoreDipendente::query() + ->where('fornitore_id', (int) $this->fornitore->id) + ->where(function ($builder) use ($rubricaId, $email, $rubrica): void { + $builder->where('rubrica_id', $rubricaId); + + if ($email !== '') { + $builder->orWhereRaw('LOWER(email) = ?', [$email]); + } + + $builder->orWhere(function ($sub) use ($rubrica): void { + $sub->where('nome', (string) ($rubrica->nome ?? '')) + ->where('cognome', (string) ($rubrica->cognome ?? '')); + }); + }); + + $dipendente = $query->first(); + if (! $dipendente instanceof FornitoreDipendente) { + $dipendente = new FornitoreDipendente(); + $dipendente->fornitore_id = (int) $this->fornitore->id; + $dipendente->created_by_user_id = Auth::id(); + } + + $dipendente->rubrica_id = $rubricaId; + $dipendente->nome = (string) ($rubrica->nome ?: $rubrica->ragione_sociale ?: 'Dipendente'); + $dipendente->cognome = trim((string) ($rubrica->cognome ?? '')) !== '' ? (string) $rubrica->cognome : null; + $dipendente->email = $email !== '' ? $email : null; + $dipendente->telefono = trim((string) ($rubrica->telefono_cellulare ?: $rubrica->telefono_ufficio ?: $rubrica->telefono_casa)) ?: null; + $dipendente->attivo = true; + $dipendente->updated_by_user_id = Auth::id(); + $dipendente->save(); + + $this->resetDipendenteRubricaSelection(); + $this->refreshDipendentiRows(); + + Notification::make()->title('Dipendente collegato dalla rubrica')->success()->send(); + } + public function abilitaAccessoDipendente(int $dipendenteId): void { $dipendente = FornitoreDipendente::query() @@ -446,7 +532,7 @@ private function loadTagSuggestions(): void private function refreshDipendentiRows(): void { $this->dipendentiRows = FornitoreDipendente::query() - ->with('user:id,name,email') + ->with(['user:id,name,email', 'rubrica:id,nome,cognome,ragione_sociale']) ->where('fornitore_id', (int) $this->fornitore->id) ->orderByDesc('attivo') ->orderBy('nome') @@ -460,6 +546,8 @@ private function refreshDipendentiRows(): void 'email' => (string) ($d->email ?? ''), 'telefono' => (string) ($d->telefono ?? ''), 'attivo' => (bool) $d->attivo, + 'rubrica_id' => (int) ($d->rubrica_id ?? 0), + 'rubrica' => $d->rubrica ? $this->formatRubricaLookupLabel($d->rubrica) : '', 'user_id' => $userId, 'user_label' => $userId > 0 ? ((string) ($d->user?->name ?: ('Utente #' . $userId))): '-', ]; @@ -467,6 +555,17 @@ private function refreshDipendentiRows(): void ->all(); } + private function searchDipendenteRubricaMatches(): void + { + $this->dipendenteRubricaMatches = $this->searchRubricaLookupMatches( + $this->dipendenteRubricaSearch, + $this->dipendenteRubricaId, + null, + 12, + (int) ($this->fornitore->amministratore_id ?? 0) + ); + } + private function refreshCatalogRows(): void { $this->catalogoProdottiRows = []; diff --git a/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php b/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php index 62a9a0e..5e656fe 100644 --- a/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php +++ b/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php @@ -87,7 +87,7 @@ class RubricaUniversaleScheda extends Page public bool $isInlineEditing = false; - public string $sideTab = 'collegamenti'; + public string $sideTab = 'dati'; /** @var array> */ public array $fornitoriCollegati = []; diff --git a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php index 9c4f9d6..05a0f3d 100644 --- a/app/Filament/Pages/Impostazioni/SchedaAmministratore.php +++ b/app/Filament/Pages/Impostazioni/SchedaAmministratore.php @@ -5,9 +5,11 @@ use App\Models\CommunicationMessage; use App\Models\Fornitore; use App\Models\FornitoreDipendente; +use App\Models\RubricaUniversale; use App\Models\Stabile; use App\Models\Ticket; use App\Models\User; +use App\Support\Livewire\SupportsRubricaLookup; use App\Support\StabileContext; use BackedEnum; use Filament\Forms\Components\FileUpload; @@ -36,6 +38,7 @@ class SchedaAmministratore extends Page implements HasForms { use InteractsWithForms; + use SupportsRubricaLookup; protected static ?string $navigationLabel = 'Scheda amministratore'; @@ -79,6 +82,13 @@ class SchedaAmministratore extends Page implements HasForms /** @var array */ public array $collaboratorePbxClickToCallEnabled = []; + public string $collaboratoreRubricaSearch = ''; + + public ?int $collaboratoreRubricaId = null; + + /** @var array> */ + public array $collaboratoreRubricaMatches = []; + public Amministratore $amministratore; public static function canAccess(): bool @@ -1347,6 +1357,81 @@ public function getCollaboratoriCentralinoRowsProperty(): array ->all(); } + public function updatedCollaboratoreRubricaSearch(): void + { + $this->searchCollaboratoreRubricaMatches(); + } + + public function selezionaCollaboratoreRubrica(int $rubricaId): void + { + $rubrica = RubricaUniversale::query()->find($rubricaId); + if (! $rubrica instanceof RubricaUniversale) { + return; + } + + $this->collaboratoreRubricaId = (int) $rubrica->id; + $this->collaboratoreRubricaSearch = $this->formatRubricaLookupLabel($rubrica); + $this->collaboratoreRubricaMatches = [$this->mapRubricaLookupRow($rubrica)]; + } + + public function resetCollaboratoreRubricaSelection(): void + { + $this->collaboratoreRubricaId = null; + $this->collaboratoreRubricaSearch = ''; + $this->collaboratoreRubricaMatches = []; + } + + public function abilitaCollaboratoreDaRubrica(): void + { + $rubricaId = (int) ($this->collaboratoreRubricaId ?? 0); + if ($rubricaId <= 0) { + Notification::make()->title('Seleziona un nominativo rubrica')->warning()->send(); + return; + } + + $rubrica = RubricaUniversale::query()->find($rubricaId); + if (! $rubrica instanceof RubricaUniversale) { + Notification::make()->title('Nominativo rubrica non trovato')->danger()->send(); + return; + } + + $email = mb_strtolower(trim((string) ($rubrica->email ?? ''))); + if ($email === '') { + Notification::make()->title('Email rubrica mancante')->warning()->body('Per creare o collegare il collaboratore serve un indirizzo email principale nella rubrica.')->send(); + return; + } + + $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first(); + $created = false; + + if (! $user instanceof User) { + $password = $this->generateTemporaryPassword(); + $user = User::query()->create([ + 'name' => trim((string) ($rubrica->nome_completo ?: $rubrica->ragione_sociale ?: 'Collaboratore Studio')), + 'email' => $email, + 'password' => Hash::make($password), + 'email_verified_at' => now(), + 'is_active' => true, + 'registration_status' => 'approved', + ]); + + $this->lastGeneratedPassword = $password; + $created = true; + } + + $user->assignRole('collaboratore'); + $this->syncCollaboratoreRubricaStabili($user); + + $this->collaboratorePbxExtension[(int) $user->id] = (string) ($user->pbx_extension ?? ''); + $this->searchCollaboratoreRubricaMatches(); + + Notification::make() + ->title('Collaboratore collegato dalla rubrica') + ->body($created ? 'Utente creato e assegnato agli stabili dell\'amministratore.' : 'Utente esistente aggiornato e assegnato agli stabili dell\'amministratore.') + ->success() + ->send(); + } + /** * @return array{extensions:array,groups:array,lines:array} */ @@ -1733,6 +1818,58 @@ private function normalizePbxMappingInput(string $value): array }, $tokens), fn(string $token): bool => $token !== ''))); } + private function searchCollaboratoreRubricaMatches(): void + { + $this->collaboratoreRubricaMatches = $this->searchRubricaLookupMatches( + $this->collaboratoreRubricaSearch, + $this->collaboratoreRubricaId, + null, + 12, + (int) $this->amministratore->id + ); + } + + /** + * @return array + */ + private function getAmministratoreStabileIds(): array + { + return $this->amministratore->stabili() + ->pluck('id') + ->map(fn($value) => (int) $value) + ->filter(fn(int $value) => $value > 0) + ->values() + ->all(); + } + + private function syncCollaboratoreRubricaStabili(User $user): void + { + if (! Schema::hasTable('collaboratore_stabile') || ! method_exists($user, 'stabiliAssegnati')) { + return; + } + + $adminStabileIds = $this->getAmministratoreStabileIds(); + if ($adminStabileIds === []) { + return; + } + + $currentIds = $user->stabiliAssegnati() + ->pluck('stabili.id') + ->map(fn($value) => (int) $value) + ->filter(fn(int $value) => $value > 0) + ->values() + ->all(); + + $outsideAdminIds = Stabile::query() + ->whereIn('id', $currentIds) + ->where('amministratore_id', '!=', (int) $this->amministratore->id) + ->pluck('id') + ->map(fn($value) => (int) $value) + ->all(); + + $user->stabiliAssegnati()->sync(array_values(array_unique(array_merge($outsideAdminIds, $adminStabileIds)))); + } + public function abilitaAccessoFornitore(int $dipendenteId): void { $dipendente = FornitoreDipendente::query() diff --git a/app/Http/Controllers/Admin/TicketAttachmentViewController.php b/app/Http/Controllers/Admin/TicketAttachmentViewController.php index 303e99e..9837e03 100644 --- a/app/Http/Controllers/Admin/TicketAttachmentViewController.php +++ b/app/Http/Controllers/Admin/TicketAttachmentViewController.php @@ -32,8 +32,14 @@ public function show(TicketAttachment $attachment): BinaryFileResponse if ($user->hasRole('fornitore')) { $this->authorizeSupplierAttachment($user, $attachment); } else { - $stabileId = StabileContext::resolveActiveStabileId($user); - if (! $stabileId || (int) $ticket->stabile_id !== (int) $stabileId) { + $allowedStabili = StabileContext::accessibleStabili($user) + ->pluck('id') + ->map(fn($value) => (int) $value) + ->filter(fn(int $value) => $value > 0) + ->values() + ->all(); + + if ($allowedStabili === [] || ! in_array((int) $ticket->stabile_id, $allowedStabili, true)) { abort(403); } } diff --git a/app/Support/Livewire/SupportsRubricaLookup.php b/app/Support/Livewire/SupportsRubricaLookup.php index 2612edf..b1add77 100644 --- a/app/Support/Livewire/SupportsRubricaLookup.php +++ b/app/Support/Livewire/SupportsRubricaLookup.php @@ -64,6 +64,8 @@ protected function mapRubricaLookupRow(RubricaUniversale $rubrica): array 'email' => trim((string) ($rubrica->email ?? '')), 'phone' => $phone, 'category' => (string) ($rubrica->categoria ?? ''), + 'cf' => trim((string) ($rubrica->codice_fiscale ?? '')), + 'piva' => trim((string) ($rubrica->partita_iva ?? '')), 'note' => Str::limit(trim((string) ($rubrica->note ?? '')), 100), ]; } @@ -71,10 +73,10 @@ protected function mapRubricaLookupRow(RubricaUniversale $rubrica): array /** * Ricerca libera su rubrica con deduplica label e fallback sulla selezione corrente. * - * @param array $categories + * @param array|null $categories * @return array> */ - protected function searchRubricaLookupMatches(string $raw, ?int $selectedId = null, array $categories = ['assicurazione', 'fornitore', 'altro'], int $limit = 12): array + protected function searchRubricaLookupMatches(string $raw, ?int $selectedId = null, ?array $categories = null, int $limit = 12, ?int $amministratoreId = null): array { $raw = $this->normalizeRubricaLookupText($raw); @@ -89,46 +91,89 @@ protected function searchRubricaLookupMatches(string $raw, ?int $selectedId = nu $needle = '%' . mb_strtolower($raw) . '%'; $digits = preg_replace('/\D+/', '', $raw) ?: ''; - $seen = []; $rows = []; - $matches = RubricaUniversale::query() + $query = RubricaUniversale::query() ->whereNull('deleted_at') - ->whereIn('categoria', $categories) ->where(function ($query) use ($needle, $digits): void { $query->whereRaw("LOWER(COALESCE(ragione_sociale, '')) LIKE ?", [$needle]) ->orWhereRaw("LOWER(COALESCE(nome, '')) LIKE ?", [$needle]) ->orWhereRaw("LOWER(COALESCE(cognome, '')) LIKE ?", [$needle]) ->orWhereRaw("LOWER(COALESCE(email, '')) LIKE ?", [$needle]) - ->orWhereRaw("LOWER(COALESCE(note, '')) LIKE ?", [$needle]); + ->orWhereRaw("LOWER(COALESCE(note, '')) LIKE ?", [$needle]) + ->orWhereRaw("LOWER(COALESCE(codice_fiscale, '')) LIKE ?", [$needle]) + ->orWhereRaw("LOWER(COALESCE(partita_iva, '')) LIKE ?", [$needle]); if ($digits !== '') { $query->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_ufficio, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", ['%' . $digits . '%']) - ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", ['%' . $digits . '%']); + ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_cellulare, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", ['%' . $digits . '%']) + ->orWhereRaw("REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(telefono_casa, ''), ' ', ''), '+', ''), '-', ''), '(', ''), ')', '') LIKE ?", ['%' . $digits . '%']); } }) + ->orderByRaw('CASE WHEN id = ? THEN 0 ELSE 1 END', [(int) ($selectedId ?? 0)]) ->orderBy('ragione_sociale') ->orderBy('cognome') ->orderBy('nome') - ->limit(80) - ->get(); + ->limit(80); + + if ($categories !== null && $categories !== []) { + $query->whereIn('categoria', $categories); + } + + if ($amministratoreId !== null && $amministratoreId > 0 && method_exists(new RubricaUniversale(), 'getTable')) { + $query->where(function ($builder) use ($amministratoreId): void { + $builder->where('amministratore_id', $amministratoreId) + ->orWhereNull('amministratore_id'); + }); + } + + $matches = $query->get(); foreach ($matches as $rubrica) { $row = $this->mapRubricaLookupRow($rubrica); - $key = mb_strtolower(trim((string) $row['label'])); - - if ($key === '' || isset($seen[$key])) { - continue; - } - - $seen[$key] = true; - $rows[] = $row; - - if (count($rows) >= $limit) { - break; - } + $row['score'] = $this->scoreRubricaLookupMatch($row, $raw, $digits, $selectedId); + $rows[] = $row; } + usort($rows, fn(array $left, array $right): int => ((int) $right['score']) <=> ((int) $left['score'])); + + $rows = array_values(array_slice(array_map(function (array $row): array { + unset($row['score']); + + return $row; + }, $rows), 0, $limit)); + return $rows; } + + /** + * @param array $row + */ + protected function scoreRubricaLookupMatch(array $row, string $raw, string $digits, ?int $selectedId = null): int + { + $score = (int) (($selectedId ?? 0) > 0 && (int) ($row['id'] ?? 0) === (int) $selectedId ? 1000 : 0); + + $haystack = mb_strtolower(implode(' ', array_filter([ + (string) ($row['label'] ?? ''), + (string) ($row['email'] ?? ''), + (string) ($row['phone'] ?? ''), + (string) ($row['cf'] ?? ''), + (string) ($row['piva'] ?? ''), + (string) ($row['category'] ?? ''), + (string) ($row['note'] ?? ''), + ]))); + + if ($raw !== '' && str_contains($haystack, mb_strtolower($raw))) { + $score += 90; + } + + if ($digits !== '') { + $phoneDigits = preg_replace('/\D+/', '', (string) ($row['phone'] ?? '')) ?: ''; + if ($phoneDigits !== '' && str_contains($phoneDigits, $digits)) { + $score += 70; + } + } + + return $score; + } } diff --git a/resources/views/filament/pages/gescon/fornitore-scheda.blade.php b/resources/views/filament/pages/gescon/fornitore-scheda.blade.php index b160cad..4217577 100644 --- a/resources/views/filament/pages/gescon/fornitore-scheda.blade.php +++ b/resources/views/filament/pages/gescon/fornitore-scheda.blade.php @@ -53,6 +53,52 @@ @endif +
+ +
Usa lo stesso lookup operativo del ticket per agganciare un nominativo già presente nella rubrica unica.
+ + @if($dipendenteRubricaId) +
+ Nominativo selezionato: {{ $dipendenteRubricaSearch }} + +
+ @endif + + @if(count($dipendenteRubricaMatches) > 0) +
+ @foreach($dipendenteRubricaMatches as $match) + + @endforeach +
+ @elseif(filled($dipendenteRubricaSearch) && ! $dipendenteRubricaId) +
Nessun nominativo trovato con questa ricerca.
+ @endif + +
+ Collega nominativo selezionato +
+
+
@@ -84,8 +130,15 @@ @if((int) ($row['user_id'] ?? 0) > 0) {{ $row['user_label'] }} (ID {{ (int) $row['user_id'] }}) + @if((int) ($row['rubrica_id'] ?? 0) > 0) +
Rubrica: {{ $row['rubrica'] ?: ('#' . (int) $row['rubrica_id']) }}
+ @endif @else - - + @if((int) ($row['rubrica_id'] ?? 0) > 0) +
{{ $row['rubrica'] ?: ('Rubrica #' . (int) $row['rubrica_id']) }}
+ @else + - + @endif @endif diff --git a/resources/views/filament/pages/gescon/rubrica-universale-scheda.blade.php b/resources/views/filament/pages/gescon/rubrica-universale-scheda.blade.php index 1912a5e..92b1fa0 100644 --- a/resources/views/filament/pages/gescon/rubrica-universale-scheda.blade.php +++ b/resources/views/filament/pages/gescon/rubrica-universale-scheda.blade.php @@ -221,8 +221,19 @@
-
- +
+
+ + + + + +
+
+ +
+ @if($sideTab === 'dati') +
Nominativo
@@ -412,16 +423,9 @@
+ @endif
-
-
- - - - -
-
@if($sideTab === 'studio') diff --git a/resources/views/filament/pages/gescon/table.blade.php b/resources/views/filament/pages/gescon/table.blade.php index 478acf5..3693976 100644 --- a/resources/views/filament/pages/gescon/table.blade.php +++ b/resources/views/filament/pages/gescon/table.blade.php @@ -58,9 +58,8 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
@@ -103,6 +102,35 @@ class="inline-flex h-4 w-4 items-center justify-center rounded-full text-slate-5 @endforeach
@endif + +
La ricerca usa lo stesso stile operativo del ticket: risultati immediati, click diretto sulla scheda e lettura per telefono, email, ragione sociale o anagrafica unica collegata.
+ + @if($this->fornitoreMatches->count() > 0) +
+ @foreach($this->fornitoreMatches as $match) + + @endforeach +
+ @elseif(trim((string) ($this->searchInput ?? '')) !== '') +
Nessun fornitore trovato con questa ricerca.
+ @endif
diff --git a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-operativita-accessi.blade.php b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-operativita-accessi.blade.php index 9a27877..fe374ad 100644 --- a/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-operativita-accessi.blade.php +++ b/resources/views/filament/pages/impostazioni/partials/scheda-amministratore-operativita-accessi.blade.php @@ -1,4 +1,52 @@
+
+
Aggancia collaboratore da anagrafica unica
+
Usa la stessa ricerca guidata del ticket per trasformare un nominativo rubrica in collaboratore studio e assegnarlo agli stabili dell'amministratore corrente.
+ + + + @if($collaboratoreRubricaId) +
+ Nominativo selezionato: {{ $collaboratoreRubricaSearch }} + +
+ @endif + + @if(count($collaboratoreRubricaMatches) > 0) +
+ @foreach($collaboratoreRubricaMatches as $match) + + @endforeach +
+ @elseif(filled($collaboratoreRubricaSearch) && ! $collaboratoreRubricaId) +
Nessun nominativo trovato con questa ricerca.
+ @endif + +
+ Abilita collaboratore dallo studio +
+
+
Centralino studio e interni collaboratori
I numeri studio e gli interni PBX/CSTA sono salvati qui. Gli interni condivisi del centralino permettono di instradare popup e chiamate anche quando il PBX espone gruppi giorno/notte invece dell'interno personale.