Fix ticket attachment visibility and unify anagrafica lookup UX

This commit is contained in:
michele 2026-04-10 12:43:41 +00:00
parent 972d2c7e83
commit ab074eb321
9 changed files with 459 additions and 39 deletions

View File

@ -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<int, array<string, mixed>> */
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 = [];

View File

@ -87,7 +87,7 @@ class RubricaUniversaleScheda extends Page
public bool $isInlineEditing = false;
public string $sideTab = 'collegamenti';
public string $sideTab = 'dati';
/** @var array<int,array<string,mixed>> */
public array $fornitoriCollegati = [];

View File

@ -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<int,bool> */
public array $collaboratorePbxClickToCallEnabled = [];
public string $collaboratoreRubricaSearch = '';
public ?int $collaboratoreRubricaId = null;
/** @var array<int,array<string,mixed>> */
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<int,string>,groups:array<int,string>,lines:array<int,string>}
*/
@ -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<int,int>
*/
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()

View File

@ -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);
}
}

View File

@ -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<int,string> $categories
* @param array<int,string>|null $categories
* @return array<int,array<string,mixed>>
*/
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<string,mixed> $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;
}
}

View File

@ -53,6 +53,52 @@
</div>
@endif
<div class="mt-4 rounded-xl border border-slate-200 bg-slate-50 p-3">
<label class="block text-sm">
<span class="mb-1 block font-medium">Ricerca nominativo in anagrafica unica</span>
<input type="text" wire:model.live.debounce.250ms="dipendenteRubricaSearch" class="w-full rounded-lg border-gray-300" placeholder="Ricerca per telefono, nome, cognome, ragione sociale o email e seleziona il contatto con un click. Es. +39 333 1234567 oppure Mario Rossi" />
</label>
<div class="mt-2 text-xs text-gray-500">Usa lo stesso lookup operativo del ticket per agganciare un nominativo già presente nella rubrica unica.</div>
@if($dipendenteRubricaId)
<div class="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-800">
Nominativo selezionato: <span class="font-semibold">{{ $dipendenteRubricaSearch }}</span>
<button type="button" wire:click="resetDipendenteRubricaSelection" class="ml-2 inline-flex items-center rounded-md bg-white px-2 py-1 text-[11px] font-medium text-emerald-700 ring-1 ring-inset ring-emerald-300 hover:bg-emerald-100">Cambia</button>
</div>
@endif
@if(count($dipendenteRubricaMatches) > 0)
<div class="mt-3 space-y-2">
@foreach($dipendenteRubricaMatches as $match)
<button type="button" wire:click="selezionaDipendenteRubrica({{ (int) $match['id'] }})" class="w-full rounded-lg border p-3 text-left {{ (int) $dipendenteRubricaId === (int) $match['id'] ? 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200' : 'bg-white hover:bg-gray-50' }}">
<div class="text-sm font-medium">{{ $match['label'] }}</div>
<div class="mt-1 text-xs text-gray-600">
{{ $match['phone'] ?: 'Telefono non valorizzato' }}
@if($match['email'])
· {{ $match['email'] }}
@endif
@if($match['cf'])
· CF {{ $match['cf'] }}
@endif
</div>
@if($match['category'])
<div class="mt-1 text-xs text-indigo-700">Categoria: {{ $match['category'] }}</div>
@endif
@if($match['note'])
<div class="mt-1 text-xs text-gray-500">{{ $match['note'] }}</div>
@endif
</button>
@endforeach
</div>
@elseif(filled($dipendenteRubricaSearch) && ! $dipendenteRubricaId)
<div class="mt-3 text-xs text-gray-500">Nessun nominativo trovato con questa ricerca.</div>
@endif
<div class="mt-3">
<x-filament::button size="sm" color="primary" type="button" wire:click="collegaDipendenteDaRubrica">Collega nominativo selezionato</x-filament::button>
</div>
</div>
<div class="mt-3 grid grid-cols-1 gap-2 md:grid-cols-4">
<input type="text" wire:model.defer="nuovoDipendenteNome" class="rounded-md border-gray-300 text-xs" placeholder="Nome" />
<input type="text" wire:model.defer="nuovoDipendenteCognome" class="rounded-md border-gray-300 text-xs" placeholder="Cognome" />
@ -84,8 +130,15 @@
<td class="border px-2 py-1.5">
@if((int) ($row['user_id'] ?? 0) > 0)
{{ $row['user_label'] }} (ID {{ (int) $row['user_id'] }})
@if((int) ($row['rubrica_id'] ?? 0) > 0)
<div class="mt-1 text-[11px] text-slate-500">Rubrica: {{ $row['rubrica'] ?: ('#' . (int) $row['rubrica_id']) }}</div>
@endif
@else
-
@if((int) ($row['rubrica_id'] ?? 0) > 0)
<div>{{ $row['rubrica'] ?: ('Rubrica #' . (int) $row['rubrica_id']) }}</div>
@else
-
@endif
@endif
</td>
<td class="border px-2 py-1.5">

View File

@ -221,8 +221,19 @@
</div>
</div>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
<x-filament::section class="lg:col-span-2">
<div class="rounded-lg border border-gray-200 bg-white p-2">
<div class="flex flex-wrap gap-2">
<button type="button" wire:click="$set('sideTab', 'dati')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'dati' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Dati condivisi</button>
<button type="button" wire:click="$set('sideTab', 'collegamenti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'collegamenti' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Collegamenti</button>
<button type="button" wire:click="$set('sideTab', 'studio')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'studio' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Collaboratore studio</button>
<button type="button" wire:click="$set('sideTab', 'dipendenti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'dipendenti' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Dipendenti</button>
<button type="button" wire:click="$set('sideTab', 'autorizzazioni')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'autorizzazioni' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Ruoli e autorizzazioni</button>
</div>
</div>
<div class="space-y-4">
@if($sideTab === 'dati')
<x-filament::section>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<div class="text-sm text-gray-500">Nominativo</div>
@ -412,16 +423,9 @@
</div>
</div>
</x-filament::section>
@endif
<div class="space-y-4">
<div class="rounded-lg border border-gray-200 bg-white p-2">
<div class="flex flex-wrap gap-2">
<button type="button" wire:click="$set('sideTab', 'collegamenti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'collegamenti' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Collegamenti</button>
<button type="button" wire:click="$set('sideTab', 'studio')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'studio' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Collaboratore studio</button>
<button type="button" wire:click="$set('sideTab', 'dipendenti')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'dipendenti' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Dipendenti</button>
<button type="button" wire:click="$set('sideTab', 'autorizzazioni')" class="rounded-md px-3 py-1.5 text-xs font-medium {{ $sideTab === 'autorizzazioni' ? 'bg-indigo-700 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200' }}">Ruoli e autorizzazioni</button>
</div>
</div>
@if($sideTab === 'studio')
<x-filament::section>

View File

@ -58,9 +58,8 @@ class="inline-flex items-center rounded-md px-3 py-1.5 text-xs font-medium {{ $t
<div class="flex w-full flex-col gap-2 md:w-[760px] md:flex-row md:items-center">
<input
type="text"
wire:model.defer="searchInput"
wire:keydown.enter="applicaRicerca"
placeholder="Ragione sociale, nome, P.IVA, CF, email, telefono, TAG"
wire:model.live.debounce.250ms="searchInput"
placeholder="Ricerca per telefono, nome, cognome, ragione sociale, email, P.IVA, CF o TAG e seleziona il fornitore con un click"
class="w-full rounded-lg border-gray-300 text-sm"
>
<div class="flex items-center gap-2">
@ -103,6 +102,35 @@ class="inline-flex h-4 w-4 items-center justify-center rounded-full text-slate-5
@endforeach
</div>
@endif
<div class="mt-2 text-xs text-gray-500">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.</div>
@if($this->fornitoreMatches->count() > 0)
<div class="mt-3 space-y-2 md:max-w-[760px]">
@foreach($this->fornitoreMatches as $match)
<button type="button" wire:click="apriScheda({{ (int) $match->id }})" class="w-full rounded-lg border p-3 text-left {{ (int) ($this->selectedFornitoreId ?? 0) === (int) $match->id ? 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200' : 'bg-white hover:bg-gray-50' }}">
<div class="text-sm font-medium">{{ $this->getFornitoreLabel($match) }}</div>
<div class="mt-1 text-xs text-gray-600">
{{ $match->telefono ?: ($match->cellulare ?: 'Telefono non valorizzato') }}
@if($match->email)
· {{ $match->email }}
@endif
@if($match->partita_iva)
· P.IVA {{ $match->partita_iva }}
@endif
</div>
@if($match->tags)
<div class="mt-1 text-xs text-indigo-700">Tag: {{ $match->tags }}</div>
@endif
@if($match->note)
<div class="mt-1 text-xs text-gray-500">{{ \\Illuminate\\Support\\Str::limit((string) $match->note, 120) }}</div>
@endif
</button>
@endforeach
</div>
@elseif(trim((string) ($this->searchInput ?? '')) !== '')
<div class="mt-3 text-xs text-gray-500">Nessun fornitore trovato con questa ricerca.</div>
@endif
</div>
</div>
</div>

View File

@ -1,4 +1,52 @@
<div class="space-y-4">
<div class="rounded-lg border border-gray-200 bg-white p-3">
<div class="mb-2 text-sm font-semibold text-gray-700">Aggancia collaboratore da anagrafica unica</div>
<div class="mb-3 text-xs text-gray-500">Usa la stessa ricerca guidata del ticket per trasformare un nominativo rubrica in collaboratore studio e assegnarlo agli stabili dell'amministratore corrente.</div>
<label class="block text-sm">
<span class="mb-1 block font-medium">Ricerca nominativo</span>
<input type="text" wire:model.live.debounce.250ms="collaboratoreRubricaSearch" class="w-full rounded-lg border-gray-300" placeholder="Ricerca per telefono, nome, cognome, ragione sociale o email e seleziona il contatto con un click. Es. +39 333 1234567 oppure Mario Rossi" />
</label>
@if($collaboratoreRubricaId)
<div class="mt-3 rounded-lg border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-800">
Nominativo selezionato: <span class="font-semibold">{{ $collaboratoreRubricaSearch }}</span>
<button type="button" wire:click="resetCollaboratoreRubricaSelection" class="ml-2 inline-flex items-center rounded-md bg-white px-2 py-1 text-[11px] font-medium text-emerald-700 ring-1 ring-inset ring-emerald-300 hover:bg-emerald-100">Cambia</button>
</div>
@endif
@if(count($collaboratoreRubricaMatches) > 0)
<div class="mt-3 space-y-2">
@foreach($collaboratoreRubricaMatches as $match)
<button type="button" wire:click="selezionaCollaboratoreRubrica({{ (int) $match['id'] }})" class="w-full rounded-lg border p-3 text-left {{ (int) $collaboratoreRubricaId === (int) $match['id'] ? 'border-emerald-400 bg-emerald-50 ring-1 ring-emerald-200' : 'bg-white hover:bg-gray-50' }}">
<div class="text-sm font-medium">{{ $match['label'] }}</div>
<div class="mt-1 text-xs text-gray-600">
{{ $match['phone'] ?: 'Telefono non valorizzato' }}
@if($match['email'])
· {{ $match['email'] }}
@endif
@if($match['cf'])
· CF {{ $match['cf'] }}
@endif
</div>
@if($match['category'])
<div class="mt-1 text-xs text-indigo-700">Categoria: {{ $match['category'] }}</div>
@endif
@if($match['note'])
<div class="mt-1 text-xs text-gray-500">{{ $match['note'] }}</div>
@endif
</button>
@endforeach
</div>
@elseif(filled($collaboratoreRubricaSearch) && ! $collaboratoreRubricaId)
<div class="mt-3 text-xs text-gray-500">Nessun nominativo trovato con questa ricerca.</div>
@endif
<div class="mt-3">
<x-filament::button size="sm" color="primary" type="button" wire:click="abilitaCollaboratoreDaRubrica">Abilita collaboratore dallo studio</x-filament::button>
</div>
</div>
<div class="rounded-lg border border-gray-200 bg-white p-3">
<div class="mb-2 text-sm font-semibold text-gray-700">Centralino studio e interni collaboratori</div>
<div class="mb-3 text-xs text-gray-500">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.</div>