> */ public array $fornitori = []; /** @var array> */ public array $stabili = []; /** @var array */ public array $collegamentoSuggerito = []; public ?int $collegamentoStabileId = null; public ?int $collegamentoUnitaId = null; public string $collegamentoRuolo = 'altro'; public string $collegamentoRuoloCustom = ''; /** @var array> */ public array $contattiPrincipali = []; /** @var array> */ public array $ruoliAttivi = []; /** @var array> */ public array $cronistoriaRuoli = []; public int $adminId = 0; public ?int $prevRecordId = null; public ?int $nextRecordId = null; public int $navPerPage = 25; public int $navPage = 1; public int $navPages = 1; /** @var array */ public array $adminStabileIds = []; public bool $isInlineEditing = false; public string $sideTab = 'collegamenti'; /** @var array> */ public array $fornitoriCollegati = []; /** @var array> */ public array $dipendentiFornitoreRows = []; /** @var array */ public array $dipendenteRubricaSelect = []; /** @var array> */ public array $dipendenteManualDraft = []; /** @var array */ public array $dipendentePbxExtension = []; /** @var array */ public array $studioCollaboratoreStabileIds = []; /** @var array */ public array $studioCollaboratoreStabileOptions = []; public ?int $studioCollaboratoreUserId = null; public ?string $studioCollaboratoreUserEmail = null; public ?string $studioCollaboratoreUserName = null; public ?string $studioCollaboratoreGeneratedPassword = null; public string $studioCollaboratorePbxExtension = ''; /** @var array */ public array $studioCollaboratoreRoles = []; /** @var array */ public array $inlineForm = []; /** @var array> */ public array $emailMultiple = []; public function mount(int | string $record): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } if (! $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])) { abort(403); } $this->rubrica = RubricaUniversale::query()->findOrFail((int) $record); $this->applyLegacyIdentityDefaults(); $activeStabile = StabileContext::getActiveStabile($user); $adminId = (int) ($activeStabile?->amministratore_id ?: 0); $this->adminId = $adminId; $stabiliAdminIds = []; if (! $user->hasRole('super-admin') && $adminId > 0) { $stabiliAdminIds = Stabile::query() ->where('amministratore_id', $adminId) ->pluck('id') ->map(fn($id) => (int) $id) ->all(); } $this->adminStabileIds = $stabiliAdminIds; $this->studioCollaboratoreStabileOptions = Stabile::query() ->when($adminId > 0, fn(Builder $q) => $q->where('amministratore_id', $adminId)) ->when($adminId <= 0 && ! empty($stabiliAdminIds), fn(Builder $q) => $q->whereIn('id', $stabiliAdminIds)) ->orderBy('codice_stabile') ->orderBy('denominazione') ->get(['id', 'codice_stabile', 'denominazione']) ->mapWithKeys(fn(Stabile $stabile) => [ (int) $stabile->id => trim((string) ($stabile->codice_stabile ?: ('#' . $stabile->id)) . ' - ' . (string) ($stabile->denominazione ?: 'Stabile')), ]) ->all(); $this->initArchivioNavigation(); $this->fornitori = Fornitore::query() ->where('amministratore_id', $adminId) ->where('rubrica_id', (int) $this->rubrica->id) ->orderBy('ragione_sociale') ->limit(50) ->get(['id', 'codice_univoco', 'ragione_sociale', 'partita_iva', 'codice_fiscale']) ->map(fn(Fornitore $f) => [ 'id' => (int) $f->id, 'codice' => (string) ($f->codice_univoco ?? ''), 'nome' => (string) ($f->ragione_sociale ?: trim((string) ($f->cognome ?? '') . ' ' . (string) ($f->nome ?? ''))), 'piva' => (string) ($f->partita_iva ?? ''), 'cf' => (string) ($f->codice_fiscale ?? ''), ]) ->all(); $ruoliQuery = RubricaRuolo::query() ->with(['stabile', 'unitaImmobiliare.stabile']) ->where('rubrica_id', (int) $this->rubrica->id); if (! $user->hasRole('super-admin') && ! empty($stabiliAdminIds)) { $ruoliQuery->where(function ($q) use ($stabiliAdminIds) { $q->whereNull('stabile_id') ->orWhereIn('stabile_id', $stabiliAdminIds); }); } $ruoli = $ruoliQuery ->orderByDesc('is_attivo') ->orderByDesc('is_preferito') ->orderByDesc('data_inizio') ->orderByDesc('id') ->limit(200) ->get(); $labels = self::getRuoliLabels(); $this->ruoliAttivi = $ruoli ->filter(fn(RubricaRuolo $r) => (bool) $r->is_attivo) ->map(function (RubricaRuolo $r) use ($labels) { $stabile = $r->stabile ?: $r->unitaImmobiliare?->stabile; $ruoloKey = (string) ($r->ruolo_standard ?? 'altro'); $ruoloLabel = (string) ($labels[$ruoloKey] ?? $ruoloKey); if (! empty($r->ruolo_custom)) { $ruoloLabel .= ' (' . (string) $r->ruolo_custom . ')'; } return [ 'ruolo' => $ruoloLabel, 'stabile_codice' => (string) ($stabile?->codice_operatore ?? $stabile?->codice_stabile ?? ''), 'stabile_nome' => (string) ($stabile?->denominazione ?? ''), 'unita_id' => (int) ($r->unitaImmobiliare?->id ?? 0), 'unita_codice' => (string) ($r->unitaImmobiliare?->codice_unita ?? ''), 'unita_nome' => (string) ($r->unitaImmobiliare?->denominazione ?? ''), 'preferito' => (bool) ($r->is_preferito ?? false), ]; }) ->values() ->all(); $this->cronistoriaRuoli = $ruoli ->map(function (RubricaRuolo $r) use ($labels) { $stabile = $r->stabile ?: $r->unitaImmobiliare?->stabile; $unita = $r->unitaImmobiliare; $ruoloKey = (string) ($r->ruolo_standard ?? 'altro'); $ruoloLabel = (string) ($labels[$ruoloKey] ?? $ruoloKey); if (! empty($r->ruolo_custom)) { $ruoloLabel .= ' (' . (string) $r->ruolo_custom . ')'; } return [ 'ruolo' => $ruoloLabel, 'data_inizio' => optional($r->data_inizio)->format('d/m/Y'), 'data_fine' => $r->data_fine ? optional($r->data_fine)->format('d/m/Y') : null, 'attivo' => (bool) ($r->is_attivo ?? true), 'preferito' => (bool) ($r->is_preferito ?? false), 'stabile_codice' => (string) ($stabile?->codice_operatore ?? $stabile?->codice_stabile ?? ''), 'stabile_nome' => (string) ($stabile?->denominazione ?? ''), 'unita_id' => (int) ($unita?->id ?? 0), 'unita_codice' => (string) ($unita?->codice_unita ?? ''), 'unita_nome' => (string) ($unita?->denominazione ?? ''), ]; }) ->all(); // Collegamenti stabili: includi sia stabili con rubrica_id diretto sia stabili legati tramite ruoli. $stabiliIds = []; foreach ($ruoli as $r) { $sid = (int) ($r->stabile_id ?? 0); if ($sid <= 0) { $sid = (int) ($r->unitaImmobiliare?->stabile_id ?? 0); } if ($sid > 0) { $stabiliIds[] = $sid; } } $stabiliIds = array_values(array_unique($stabiliIds)); $stabiliQuery = Stabile::query()->where('amministratore_id', $adminId); if (! empty($stabiliIds)) { $stabiliQuery->where(function (Builder $q) use ($stabiliIds): void { $q->whereIn('id', $stabiliIds) ->orWhere('rubrica_id', (int) $this->rubrica->id); }); } else { $stabiliQuery->where('rubrica_id', (int) $this->rubrica->id); } $this->stabili = $stabiliQuery ->orderBy('codice_stabile') ->limit(100) ->get(['id', 'codice_stabile', 'cod_stabile', 'denominazione', 'codice_fiscale']) ->map(fn(Stabile $s) => [ 'id' => (int) $s->id, 'codice' => (string) ($s->codice_operatore ?? $s->codice_stabile ?? ''), 'nome' => (string) ($s->denominazione ?? ''), 'cf' => (string) ($s->codice_fiscale ?? ''), ]) ->all(); // Contatti principali (per mostrarli velocemente nella scheda): prende canali principali + fallback ai campi telefono/email. if (Schema::hasTable('rubrica_contatto_canali')) { $principali = RubricaContattoCanale::query() ->where('rubrica_id', (int) $this->rubrica->id) ->orderByDesc('is_principale') ->orderByDesc('data_inizio') ->orderByDesc('id') ->limit(20) ->get(); $this->contattiPrincipali = $principali ->map(fn(RubricaContattoCanale $c) => [ 'tipo' => (string) ($c->tipo ?? 'altro'), 'etichetta' => (string) ($c->etichetta ?? ''), 'valore' => (string) ($c->valore ?? ''), 'principale' => (bool) ($c->is_principale ?? false), ]) ->all(); } else { $this->contattiPrincipali = []; } $this->loadEmailMultiple(); $this->fillInlineForm(); $this->hydrateCollegamentoWorkspace(); $this->hydrateFornitoriWorkspace(); $this->hydrateStudioCollaboratoreWorkspace(); } private function hydrateCollegamentoWorkspace(): void { $suggestion = $this->resolveCollegamentoSuggerito(); $this->collegamentoSuggerito = $suggestion; $this->collegamentoStabileId = ! empty($suggestion['stabile_id']) ? (int) $suggestion['stabile_id'] : null; $this->collegamentoUnitaId = ! empty($suggestion['unita_id']) ? (int) $suggestion['unita_id'] : null; $this->collegamentoRuolo = ! empty($suggestion['role_standard']) ? (string) $suggestion['role_standard'] : 'altro'; $this->collegamentoRuoloCustom = ! empty($suggestion['role_custom']) ? (string) $suggestion['role_custom'] : ''; } public function collegaCollegamentoSuggerito(): void { if ($this->collegamentoSuggerito === []) { Notification::make() ->title('Nessun collegamento suggerito disponibile') ->warning() ->send(); return; } $this->persistCollegamentoRubrica( (int) ($this->collegamentoSuggerito['stabile_id'] ?? 0), ! empty($this->collegamentoSuggerito['unita_id']) ? (int) $this->collegamentoSuggerito['unita_id'] : null, (string) ($this->collegamentoSuggerito['role_standard'] ?? 'altro'), (string) ($this->collegamentoSuggerito['role_custom'] ?? ''), (string) ($this->collegamentoSuggerito['source'] ?? 'suggerimento_scheda') ); } public function salvaCollegamentoManuale(): void { $data = validator([ 'stabile_id' => $this->collegamentoStabileId, 'unita_immobiliare_id' => $this->collegamentoUnitaId, 'ruolo_standard' => $this->collegamentoRuolo, 'ruolo_custom' => $this->collegamentoRuoloCustom, ], [ 'stabile_id' => ['required', 'integer'], 'unita_immobiliare_id' => ['nullable', 'integer'], 'ruolo_standard' => ['required', 'string', 'max:120'], 'ruolo_custom' => ['nullable', 'string', 'max:255'], ])->validate(); $this->persistCollegamentoRubrica( (int) $data['stabile_id'], ! empty($data['unita_immobiliare_id']) ? (int) $data['unita_immobiliare_id'] : null, (string) $data['ruolo_standard'], (string) ($data['ruolo_custom'] ?? ''), 'manuale_scheda' ); } private function persistCollegamentoRubrica( int $stabileId, ?int $unitaId, string $ruoloStandard, string $ruoloCustom = '', string $source = 'manuale_scheda' ): void { $stabile = Stabile::query()->find($stabileId); if (! $stabile) { Notification::make() ->title('Stabile non trovato') ->warning() ->send(); return; } $user = Auth::user(); $isSuperAdmin = $user instanceof User && method_exists($user, 'hasRole') ? (bool) $user->hasRole('super-admin') : false; if (! $isSuperAdmin && $this->adminId > 0 && (int) $stabile->amministratore_id !== $this->adminId) { Notification::make() ->title('Stabile fuori dal tenant attivo') ->body('Il collegamento può essere creato solo sugli stabili dell\'amministratore corrente.') ->danger() ->send(); return; } $unita = null; if ($unitaId !== null && $unitaId > 0) { $unita = UnitaImmobiliare::query() ->where('id', $unitaId) ->where('stabile_id', (int) $stabile->id) ->first(); if (! $unita) { Notification::make() ->title('Unità non coerente con lo stabile') ->warning() ->send(); return; } } $ruoloStandard = trim($ruoloStandard) !== '' ? trim($ruoloStandard) : 'altro'; $ruoloCustom = trim($ruoloCustom); $attributes = [ 'rubrica_id' => (int) $this->rubrica->id, 'ruolo_standard' => $ruoloStandard, 'stabile_id' => (int) $stabile->id, 'unita_immobiliare_id' => $unita?->id, ]; if ($ruoloCustom !== '') { $attributes['ruolo_custom'] = $ruoloCustom; } $existing = RubricaRuolo::query() ->where('rubrica_id', (int) $this->rubrica->id) ->where('ruolo_standard', $ruoloStandard) ->where('stabile_id', (int) $stabile->id) ->where('unita_immobiliare_id', $unita?->id) ->when($ruoloCustom !== '', fn(Builder $query) => $query->where('ruolo_custom', $ruoloCustom)) ->first(); $payload = [ 'ruolo_custom' => $ruoloCustom !== '' ? $ruoloCustom : null, 'is_attivo' => true, 'is_preferito' => $existing ? (bool) ($existing->is_preferito ?? false) : count($this->ruoliAttivi) === 0, 'data_inizio' => $existing?->data_inizio ?: now()->toDateString(), 'meta' => array_filter([ 'source' => $source, 'linked_from' => 'rubrica_scheda', ], fn($value) => $value !== null && $value !== ''), ]; if ($existing) { $existing->fill($payload); $existing->save(); } else { RubricaRuolo::query()->create(array_merge($attributes, $payload)); } $this->rubrica->riferimento_stabile = $this->buildLocalStabileReference($stabile); $this->rubrica->riferimento_unita = $unita ? $this->buildLocalUnitReference($unita) : $this->rubrica->riferimento_unita; if (in_array($ruoloStandard, ['condomino', 'inquilino', 'fornitore', 'amministratore', 'altro'], true)) { $this->rubrica->tipo_utenza_call = $ruoloStandard; } $this->rubrica->data_ultima_modifica = now()->toDateString(); $this->rubrica->modificato_da = Auth::id(); $this->rubrica->save(); $this->mount((int) $this->rubrica->id); Notification::make() ->title('Collegamento aggiornato') ->body('La scheda rubrica è ora agganciata a stabile e unità selezionati.') ->success() ->send(); } /** @return array */ private function resolveCollegamentoSuggerito(): array { $legacy = $this->parseCollegamentoLegacyMeta((string) ($this->rubrica->note ?? '')); $stabile = $this->resolveCollegamentoStabile($legacy['cod_stabile'] ?? null, (string) ($this->rubrica->riferimento_stabile ?? '')); if (empty($stabile['id'])) { return []; } $unit = $this->resolveCollegamentoUnita( (int) $stabile['id'], $legacy, (string) ($this->rubrica->riferimento_unita ?? '') ); return array_filter([ 'stabile_id' => (int) $stabile['id'], 'stabile_label' => $this->buildCollegamentoStabileReference($stabile), 'unita_id' => (int) ($unit['unit_id'] ?? 0) ?: null, 'unita_label' => (string) ($unit['unit_label'] ?? ''), 'unita_reference' => (string) ($unit['unit_reference'] ?? ''), 'role_standard' => $this->mapCollegamentoRole($legacy['ruolo'] ?? $this->rubrica->tipo_utenza_call), 'role_custom' => null, 'source' => (string) ($unit['source'] ?? 'riferimento_stabile'), 'requires_review' => (bool) ($unit['source'] ?? false), ], fn($value) => $value !== null && $value !== ''); } /** @return array{cod_stabile:?string,cod_cond:?string,ruolo:?string} */ private function parseCollegamentoLegacyMeta(string $note): array { $payload = [ 'cod_stabile' => null, 'cod_cond' => null, 'ruolo' => null, ]; foreach (array_keys($payload) as $key) { if (preg_match('/' . preg_quote($key, '/') . '=([^|\n\r]+)/i', $note, $matches) === 1) { $payload[$key] = trim((string) ($matches[1] ?? '')) ?: null; } } return $payload; } /** @return array{id:int|null,codice_stabile:?string,cod_stabile:?string,denominazione:?string} */ private function resolveCollegamentoStabile(?string $legacyCode, string $reference): array { $blank = ['id' => null, 'codice_stabile' => null, 'cod_stabile' => null, 'denominazione' => null]; foreach (array_filter([$legacyCode, ...$this->extractReferenceCandidates($reference)]) as $candidate) { $code = trim((string) $candidate); if ($code === '') { continue; } $trimmed = ltrim($code, '0'); $row = Stabile::query() ->when($this->adminId > 0, fn(Builder $query) => $query->where('amministratore_id', $this->adminId)) ->where(function (Builder $query) use ($code, $trimmed): void { $query->where('codice_stabile', $code) ->orWhere('cod_stabile', $code); if ($trimmed !== '' && $trimmed !== $code) { $query->orWhere('codice_stabile', $trimmed) ->orWhere('cod_stabile', $trimmed); } }) ->orderBy('id') ->first(['id', 'codice_stabile', 'cod_stabile', 'denominazione']); if ($row) { return [ 'id' => (int) $row->id, 'codice_stabile' => $row->codice_stabile ? (string) $row->codice_stabile : null, 'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null, 'denominazione' => $row->denominazione ? (string) $row->denominazione : null, ]; } } $name = $this->extractReferenceName($reference); if ($name !== '') { $rows = Stabile::query() ->when($this->adminId > 0, fn(Builder $query) => $query->where('amministratore_id', $this->adminId)) ->whereRaw("LOWER(COALESCE(denominazione, '')) = ?", [mb_strtolower($name)]) ->limit(2) ->get(['id', 'codice_stabile', 'cod_stabile', 'denominazione']); if ($rows->count() === 1) { $row = $rows->first(); return [ 'id' => (int) $row->id, 'codice_stabile' => $row->codice_stabile ? (string) $row->codice_stabile : null, 'cod_stabile' => $row->cod_stabile ? (string) $row->cod_stabile : null, 'denominazione' => $row->denominazione ? (string) $row->denominazione : null, ]; } } return $blank; } /** @return array */ private function resolveCollegamentoUnita(int $stabileId, array $legacy, string $reference): array { $reference = trim($reference); if ($reference !== '') { $code = trim((string) Str::before($reference, ' - ')); if ($code !== '') { $rows = UnitaImmobiliare::query() ->where('stabile_id', $stabileId) ->where('codice_unita', $code) ->limit(2) ->get(['id', 'codice_unita', 'denominazione', 'scala', 'interno']); if ($rows->count() === 1) { $row = $rows->first(); return [ 'unit_id' => (int) $row->id, 'unit_label' => $this->buildLocalUnitLabel($row), 'unit_reference' => $this->buildLocalUnitReference($row), 'source' => 'riferimento_unita', ]; } } } if (! empty($legacy['cod_stabile']) && ! empty($legacy['cod_cond']) && $this->hasGesconImportCondomin()) { $rows = DB::connection('gescon_import') ->table('condomin') ->where('cod_stabile', trim((string) $legacy['cod_stabile'])) ->where('cod_cond', trim((string) $legacy['cod_cond'])) ->select(['nom_cond', 'scala', 'interno']) ->get(); $candidates = []; foreach ($rows as $row) { $unit = $this->resolveUnitFromImportRow($stabileId, [ 'nom_cond' => (string) ($row->nom_cond ?? ''), 'scala' => (string) ($row->scala ?? ''), 'interno' => (string) ($row->interno ?? ''), ]); if (! $unit) { continue; } $candidates[(int) $unit->unit_id] = [ 'unit_id' => (int) $unit->unit_id, 'unit_label' => $this->buildLocalUnitLabel($unit), 'unit_reference' => $this->buildLocalUnitReference($unit), 'source' => 'gescon_import', ]; } if (count($candidates) === 1) { return array_values($candidates)[0]; } } return []; } /** @return array */ private function extractReferenceCandidates(string $reference): array { $parts = preg_split('/\s*-\s*/', trim($reference)) ?: []; return array_values(array_filter(array_map(static function (string $part): string { $value = trim($part); return preg_match('/^[0-9]+$/', $value) === 1 ? $value : ''; }, $parts))); } private function extractReferenceName(string $reference): string { $parts = preg_split('/\s*-\s*/', trim($reference)) ?: []; $parts = array_values(array_filter(array_map(static fn(string $part): string => trim($part), $parts), static fn(string $part): bool => $part !== '' && preg_match('/^[0-9]+$/', $part) !== 1)); return $parts !== [] ? (string) end($parts) : ''; } private function buildLocalStabileReference(Stabile $stabile): string { $parts = array_values(array_filter([ trim((string) ($stabile->cod_stabile ?? '')), trim((string) ($stabile->codice_stabile ?? '')), trim((string) ($stabile->denominazione ?? '')), ])); return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) $stabile->id); } private function buildCollegamentoStabileReference(array $stabile): string { $parts = array_values(array_filter([ trim((string) ($stabile['cod_stabile'] ?? '')), trim((string) ($stabile['codice_stabile'] ?? '')), trim((string) ($stabile['denominazione'] ?? '')), ])); return $parts !== [] ? implode(' - ', $parts) : ('Stabile #' . (int) ($stabile['id'] ?? 0)); } private function buildLocalUnitReference(object $unit): string { $code = trim((string) data_get($unit, 'codice_unita', '')); $label = trim((string) data_get($unit, 'denominazione', '')); return trim($code . ($label !== '' ? ' - ' . $label : '')); } private function buildLocalUnitLabel(object $unit): string { $bits = array_values(array_filter([ trim((string) data_get($unit, 'codice_unita', '')), trim((string) data_get($unit, 'scala', '')), trim((string) data_get($unit, 'interno', '')), trim((string) data_get($unit, 'denominazione', '')), ])); return implode(' / ', $bits); } private function mapCollegamentoRole(?string $value): ?string { $normalized = strtolower(trim((string) $value)); return match ($normalized) { 'c', 'condomino', 'proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'titolare' => 'condomino', 'i', 'inquilino', 'conduttore', 'locatario' => 'inquilino', 'fornitore' => 'fornitore', 'amministratore' => 'amministratore', 'collaboratore' => 'collaboratore', 'altro' => 'altro', default => null, }; } private function resolveUnitFromImportRow(int $stabileId, array $row): ?object { $query = DB::table('unita_immobiliari') ->where('stabile_id', $stabileId) ->select(['id as unit_id', 'codice_unita', 'denominazione', 'scala', 'interno']); $scala = trim((string) ($row['scala'] ?? '')); $interno = trim((string) ($row['interno'] ?? '')); if ($scala !== '' || $interno !== '') { $strict = (clone $query) ->when($scala !== '', fn($inner) => $inner->where('scala', $scala)) ->when($interno !== '', fn($inner) => $inner->where('interno', $interno)) ->get(); if ($strict->count() === 1) { return $strict->first(); } } $normalizedName = $this->normalizeCollegamentoText((string) ($row['nom_cond'] ?? '')); if ($normalizedName === '') { return null; } $matches = (clone $query) ->get() ->filter(function ($unit) use ($normalizedName): bool { $haystack = $this->normalizeCollegamentoText((string) ($unit->denominazione ?? '')); return $haystack !== '' && ($haystack === $normalizedName || str_contains($haystack, $normalizedName) || str_contains($normalizedName, $haystack)); }) ->values(); return $matches->count() === 1 ? $matches->first() : null; } private function hasGesconImportCondomin(): bool { try { return Schema::connection('gescon_import')->hasTable('condomin'); } catch (\Throwable) { return false; } } private function normalizeCollegamentoText(string $value): string { $upper = mb_strtoupper(trim($value)); $normalized = preg_replace('/[^A-Z0-9]+/u', '', $upper); return is_string($normalized) ? $normalized : ''; } private function applyLegacyIdentityDefaults(): void { $legacyIdentity = $this->extractLegacyIdentityFromNote($this->rubrica->note ?? null); if (! $this->rubrica->titolo_id && ! empty($legacyIdentity['titolo_id'])) { $this->rubrica->titolo_id = (int) $legacyIdentity['titolo_id']; $this->rubrica->saveQuietly(); $this->rubrica->refresh(); } } public function startInlineEdit(): void { $this->fillInlineForm(); $this->isInlineEditing = true; } public function cancelInlineEdit(): void { $this->fillInlineForm(); $this->isInlineEditing = false; } public function saveInlineEdit(): void { $legacyIdentity = $this->extractLegacyIdentityFromNote($this->inlineForm['note'] ?? null); $resolvedAddress = $this->parseAddressComponents($this->inlineForm['indirizzo_completo'] ?? null); $data = validator($this->inlineForm, [ 'nome' => ['nullable', 'string', 'max:255'], 'cognome' => ['nullable', 'string', 'max:255'], 'ragione_sociale' => ['nullable', 'string', 'max:255'], 'tipo_contatto' => ['nullable', 'string', 'max:120'], 'categoria' => ['nullable', 'string', 'max:120'], 'codice_fiscale' => ['nullable', 'string', 'max:32'], 'partita_iva' => ['nullable', 'string', 'max:32'], 'sesso' => ['nullable', 'string', 'max:1'], 'data_nascita' => ['nullable', 'date'], 'luogo_nascita' => ['nullable', 'string', 'max:100'], 'provincia_nascita' => ['nullable', 'string', 'max:2'], 'email' => ['nullable', 'string', 'max:255'], 'pec' => ['nullable', 'string', 'max:255'], 'telefono_ufficio' => ['nullable', 'string', 'max:64'], 'telefono_cellulare' => ['nullable', 'string', 'max:64'], 'telefono_casa' => ['nullable', 'string', 'max:64'], 'indirizzo' => ['nullable', 'string', 'max:255'], 'civico' => ['nullable', 'string', 'max:32'], 'cap' => ['nullable', 'string', 'max:5'], 'citta' => ['nullable', 'string', 'max:100'], 'provincia' => ['nullable', 'string', 'max:2'], 'nazione' => ['nullable', 'string', 'max:50'], 'indirizzo_completo' => ['nullable', 'string', 'max:255'], 'tipo_utenza_call' => ['nullable', 'string', 'max:120'], 'riferimento_stabile' => ['nullable', 'string', 'max:255'], 'riferimento_unita' => ['nullable', 'string', 'max:255'], 'note' => ['nullable', 'string', 'max:5000'], 'note_segreteria' => ['nullable', 'string', 'max:5000'], ])->validate(); $normalizedBirthDate = $this->normalizeLegacyDateValue($data['data_nascita'] ?? $legacyIdentity['data_nascita'] ?? null); $cleanNote = $this->cleanNullable($legacyIdentity['note_clean'] ?? ($data['note'] ?? null)); $sesso = $this->normalizeSessoValue($data['sesso'] ?? $legacyIdentity['sesso'] ?? null); $provinciaNascita = $this->normalizeProvince($data['provincia_nascita'] ?? $legacyIdentity['provincia_nascita'] ?? null); $indirizzo = $this->cleanNullable($data['indirizzo'] ?? null) ?: ($resolvedAddress['indirizzo'] ?? null); $civico = $this->cleanNullable($data['civico'] ?? null) ?: ($resolvedAddress['civico'] ?? null); $cap = $this->cleanNullable($data['cap'] ?? null) ?: ($resolvedAddress['cap'] ?? null); $citta = $this->cleanNullable($data['citta'] ?? null) ?: ($resolvedAddress['citta'] ?? null); $provincia = $this->normalizeProvince($data['provincia'] ?? null) ?: ($resolvedAddress['provincia'] ?? null); $this->rubrica->fill([ 'nome' => $this->cleanNullable($data['nome'] ?? null), 'cognome' => $this->cleanNullable($data['cognome'] ?? null), 'ragione_sociale' => $this->cleanNullable($data['ragione_sociale'] ?? null), 'tipo_contatto' => $this->cleanNullable($data['tipo_contatto'] ?? null), 'categoria' => $this->cleanNullable($data['categoria'] ?? null), 'codice_fiscale' => $this->cleanNullable($data['codice_fiscale'] ?? null), 'partita_iva' => $this->cleanNullable($data['partita_iva'] ?? null), 'sesso' => $sesso, 'data_nascita' => $normalizedBirthDate, 'luogo_nascita' => $this->cleanNullable($data['luogo_nascita'] ?? $legacyIdentity['luogo_nascita'] ?? null), 'provincia_nascita' => $provinciaNascita, 'email' => $this->cleanNullable($data['email'] ?? null), 'pec' => $this->cleanNullable($data['pec'] ?? null), 'telefono_ufficio' => $this->cleanNullable($data['telefono_ufficio'] ?? null), 'telefono_cellulare' => $this->cleanNullable($data['telefono_cellulare'] ?? null), 'telefono_casa' => $this->cleanNullable($data['telefono_casa'] ?? null), 'indirizzo' => $indirizzo, 'civico' => $civico, 'cap' => $cap, 'citta' => $citta, 'provincia' => $provincia, 'nazione' => $this->cleanNullable($data['nazione'] ?? null) ?: $this->rubrica->nazione, 'tipo_utenza_call' => $this->cleanNullable($data['tipo_utenza_call'] ?? null), 'riferimento_stabile' => $this->cleanNullable($data['riferimento_stabile'] ?? null), 'riferimento_unita' => $this->cleanNullable($data['riferimento_unita'] ?? null), 'note' => $cleanNote, 'note_segreteria' => $this->cleanNullable($data['note_segreteria'] ?? null), 'data_ultima_modifica' => now()->toDateString(), 'modificato_da' => Auth::id(), ]); if (! $this->rubrica->titolo_id && ! empty($legacyIdentity['titolo_id'])) { $this->rubrica->titolo_id = (int) $legacyIdentity['titolo_id']; } $this->rubrica->save(); $this->mount((int) $this->rubrica->id); $this->isInlineEditing = false; Notification::make() ->title('Contatto aggiornato') ->success() ->send(); } private function initArchivioNavigation(): void { $pp = (int) request()->query('pp', 25); if ($pp <= 0) { $pp = 25; } if (! in_array($pp, [10, 25, 50, 100], true)) { $pp = 25; } $this->navPerPage = $pp; if ($this->adminId <= 0) { $this->prevRecordId = null; $this->nextRecordId = null; $this->navPage = 1; $this->navPages = 1; return; } $base = RubricaUniversale::query()->where('amministratore_id', $this->adminId); $total = (clone $base)->count(); $this->navPages = max(1, (int) ceil($total / $this->navPerPage)); $before = (clone $base) ->where($this->whereBeforeCurrent()) ->count(); $position = $before + 1; $this->navPage = max(1, (int) ceil($position / $this->navPerPage)); $this->prevRecordId = (clone $base) ->where($this->whereBeforeCurrent()) ->tap(fn(Builder $q) => $this->applySort($q, 'desc')) ->value('id'); $this->nextRecordId = (clone $base) ->where($this->whereAfterCurrent()) ->tap(fn(Builder $q) => $this->applySort($q, 'asc')) ->value('id'); } private function applySort(Builder $query, string $direction): void { $dir = strtolower($direction) === 'desc' ? 'DESC' : 'ASC'; $query ->orderByRaw("COALESCE(ragione_sociale, '') {$dir}") ->orderByRaw("COALESCE(cognome, '') {$dir}") ->orderByRaw("COALESCE(nome, '') {$dir}") ->orderBy('id', $dir); } private function whereBeforeCurrent(): \Closure { $ragione = (string) ($this->rubrica->ragione_sociale ?? ''); $cognome = (string) ($this->rubrica->cognome ?? ''); $nome = (string) ($this->rubrica->nome ?? ''); $id = (int) $this->rubrica->id; return function (Builder $q) use ($ragione, $cognome, $nome, $id): void { $q->whereRaw("COALESCE(ragione_sociale, '') < ?", [$ragione]) ->orWhere(function (Builder $q) use ($ragione, $cognome): void { $q->whereRaw("COALESCE(ragione_sociale, '') = ?", [$ragione]) ->whereRaw("COALESCE(cognome, '') < ?", [$cognome]); }) ->orWhere(function (Builder $q) use ($ragione, $cognome, $nome): void { $q->whereRaw("COALESCE(ragione_sociale, '') = ?", [$ragione]) ->whereRaw("COALESCE(cognome, '') = ?", [$cognome]) ->whereRaw("COALESCE(nome, '') < ?", [$nome]); }) ->orWhere(function (Builder $q) use ($ragione, $cognome, $nome, $id): void { $q->whereRaw("COALESCE(ragione_sociale, '') = ?", [$ragione]) ->whereRaw("COALESCE(cognome, '') = ?", [$cognome]) ->whereRaw("COALESCE(nome, '') = ?", [$nome]) ->where('id', '<', $id); }); }; } private function whereAfterCurrent(): \Closure { $ragione = (string) ($this->rubrica->ragione_sociale ?? ''); $cognome = (string) ($this->rubrica->cognome ?? ''); $nome = (string) ($this->rubrica->nome ?? ''); $id = (int) $this->rubrica->id; return function (Builder $q) use ($ragione, $cognome, $nome, $id): void { $q->whereRaw("COALESCE(ragione_sociale, '') > ?", [$ragione]) ->orWhere(function (Builder $q) use ($ragione, $cognome): void { $q->whereRaw("COALESCE(ragione_sociale, '') = ?", [$ragione]) ->whereRaw("COALESCE(cognome, '') > ?", [$cognome]); }) ->orWhere(function (Builder $q) use ($ragione, $cognome, $nome): void { $q->whereRaw("COALESCE(ragione_sociale, '') = ?", [$ragione]) ->whereRaw("COALESCE(cognome, '') = ?", [$cognome]) ->whereRaw("COALESCE(nome, '') > ?", [$nome]); }) ->orWhere(function (Builder $q) use ($ragione, $cognome, $nome, $id): void { $q->whereRaw("COALESCE(ragione_sociale, '') = ?", [$ragione]) ->whereRaw("COALESCE(cognome, '') = ?", [$cognome]) ->whereRaw("COALESCE(nome, '') = ?", [$nome]) ->where('id', '>', $id); }); }; } public function getPrevUrl(): ?string { if (! $this->prevRecordId) { return null; } return self::getUrl(['record' => $this->prevRecordId], panel: 'admin-filament') . '?pp=' . $this->navPerPage; } public function getNextUrl(): ?string { if (! $this->nextRecordId) { return null; } return self::getUrl(['record' => $this->nextRecordId], panel: 'admin-filament') . '?pp=' . $this->navPerPage; } public function getPrevPageUrl(): ?string { $target = $this->navPage - 1; if ($target < 1) { return null; } return $this->getPageUrl($target); } public function getNextPageUrl(): ?string { $target = $this->navPage + 1; if ($target > $this->navPages) { return null; } return $this->getPageUrl($target); } public function getPageUrl(int $page): ?string { if ($this->adminId <= 0) { return null; } $page = max(1, min($page, $this->navPages)); $offset = ($page - 1) * $this->navPerPage; $base = RubricaUniversale::query() ->where('amministratore_id', $this->adminId); $this->applySort($base, 'asc'); $id = (int) ($base->skip($offset)->take(1)->value('id') ?? 0); if ($id <= 0) { return null; } return self::getUrl(['record' => $id], panel: 'admin-filament') . '?pp=' . $this->navPerPage; } public function getPerPageUrl(int $perPage): string { if (! in_array($perPage, [10, 25, 50, 100], true)) { $perPage = 25; } return self::getUrl(['record' => (int) $this->rubrica->id], panel: 'admin-filament') . '?pp=' . $perPage; } protected function getHeaderActions(): array { return [ Action::make('titolo') ->label('Titolo') ->icon('heroicon-o-tag') ->form([ Select::make('titolo_id') ->label('Titolo') ->native(false) ->searchable() ->options(fn() => $this->getTitoliOptions()), ]) ->fillForm(fn(): array=> [ 'titolo_id' => $this->rubrica->titolo_id ? (int) $this->rubrica->titolo_id : null, ]) ->action(function (array $data): void { $titoloId = isset($data['titolo_id']) && is_numeric($data['titolo_id']) ? (int) $data['titolo_id'] : null; $this->rubrica->titolo_id = $titoloId; $this->rubrica->data_ultima_modifica = now()->toDateString(); $this->rubrica->modificato_da = Auth::id(); $this->rubrica->save(); $this->mount((int) $this->rubrica->id); Notification::make() ->title('Titolo aggiornato') ->success() ->send(); }), Action::make('contatti') ->label('Contatti') ->icon('heroicon-o-phone') ->action(function (): void { $this->startInlineEdit(); }), Action::make('email_multiple') ->label('Email aggiuntive') ->icon('heroicon-o-envelope') ->modalWidth('4xl') ->form([ Repeater::make('emails') ->label('Email aggiuntive') ->defaultItems(0) ->reorderable(true) ->schema([ Hidden::make('id'), TextInput::make('email') ->label('Email') ->email() ->required() ->maxLength(255) ->columnSpan(6), Select::make('tipo_email') ->label('Tipo') ->native(false) ->options([ 'secondaria' => 'Secondaria', 'lavoro' => 'Lavoro', 'personale' => 'Personale', 'pec' => 'PEC', 'altro' => 'Altro', ]) ->default('secondaria') ->columnSpan(4), Toggle::make('attiva') ->label('Attiva') ->default(true) ->columnSpan(2), ]) ->columns(12), ]) ->fillForm(fn(): array=> ['emails' => $this->getAdditionalEmailsFormRows()]) ->action(function (array $data): void { $this->saveAdditionalEmails($data['emails'] ?? []); $this->mount((int) $this->rubrica->id); Notification::make() ->title('Email aggiuntive aggiornate') ->success() ->send(); }), Action::make('contatti_avanzati') ->label('Contatti avanzati') ->icon('heroicon-o-rectangle-stack') ->visible(fn(): bool => Schema::hasTable('rubrica_contatto_canali')) ->modalWidth('7xl') ->form([ Repeater::make('canali') ->label('Contatti aggiuntivi') ->defaultItems(0) ->reorderable(true) ->schema([ Hidden::make('id'), Select::make('tipo') ->label('Tipo') ->options([ 'telefono' => 'Telefono', 'cellulare' => 'Cellulare', 'email' => 'Email', 'pec' => 'PEC', 'fax' => 'Fax', 'sito_web' => 'Sito web', 'altro' => 'Altro', ]) ->native(false) ->required() ->columnSpan(2), TextInput::make('etichetta') ->label('Etichetta') ->maxLength(50) ->columnSpan(2), TextInput::make('valore') ->label('Valore') ->required() ->maxLength(255) ->columnSpan(4), Toggle::make('is_principale') ->label('Principale') ->default(false) ->columnSpan(2), Select::make('stabile_id') ->label('Stabile (opzionale)') ->native(false) ->searchable() ->options(fn() => $this->getStabiliOptions()) ->reactive() ->afterStateUpdated(fn($set) => $set('unita_immobiliare_id', null)) ->columnSpan(3), Select::make('unita_immobiliare_id') ->label('Unità (opzionale)') ->native(false) ->searchable() ->options(fn($get) => $this->getUnitaOptions($get('stabile_id'))) ->columnSpan(3), DatePicker::make('data_inizio') ->label('Data inizio') ->columnSpan(2), DatePicker::make('data_fine') ->label('Data fine') ->columnSpan(2), TextInput::make('note') ->label('Note') ->maxLength(255) ->columnSpan(12), ]) ->columns(12), ]) ->fillForm(function (): array { $canali = RubricaContattoCanale::query() ->where('rubrica_id', (int) $this->rubrica->id) ->orderByDesc('is_principale') ->orderByDesc('data_inizio') ->orderByDesc('id') ->limit(200) ->get(); return [ 'canali' => $canali->map(fn(RubricaContattoCanale $c) => [ 'id' => (int) $c->id, 'tipo' => (string) ($c->tipo ?? 'altro'), 'etichetta' => (string) ($c->etichetta ?? ''), 'valore' => (string) ($c->valore ?? ''), 'is_principale' => (bool) ($c->is_principale ?? false), 'stabile_id' => $c->stabile_id ? (int) $c->stabile_id : null, 'unita_immobiliare_id' => $c->unita_immobiliare_id ? (int) $c->unita_immobiliare_id : null, 'data_inizio' => $c->data_inizio?->format('Y-m-d'), 'data_fine' => $c->data_fine?->format('Y-m-d'), 'note' => (string) (($c->meta['note'] ?? null) ?: ''), ])->all(), ]; }) ->action(function (array $data): void { /** @var array> $rows */ $rows = $data['canali'] ?? []; $rows = is_array($rows) ? array_values($rows) : []; DB::transaction(function () use ($rows): void { $keepIds = []; foreach ($rows as $row) { $id = (int) ($row['id'] ?? 0); $tipo = (string) ($row['tipo'] ?? 'altro'); if ($tipo === '') { $tipo = 'altro'; } $attrs = [ 'rubrica_id' => (int) $this->rubrica->id, 'tipo' => $tipo, 'etichetta' => ! empty($row['etichetta']) ? (string) $row['etichetta'] : null, 'valore' => ! empty($row['valore']) ? (string) $row['valore'] : null, 'is_principale' => (bool) ($row['is_principale'] ?? false), 'stabile_id' => ! empty($row['stabile_id']) ? (int) $row['stabile_id'] : null, 'unita_immobiliare_id' => ! empty($row['unita_immobiliare_id']) ? (int) $row['unita_immobiliare_id'] : null, 'data_inizio' => $row['data_inizio'] ?? null, 'data_fine' => $row['data_fine'] ?? null, 'meta' => [ 'note' => ! empty($row['note']) ? (string) $row['note'] : null, ], ]; if ($id > 0) { $existing = RubricaContattoCanale::query() ->where('id', $id) ->where('rubrica_id', (int) $this->rubrica->id) ->first(); if ($existing) { $existing->fill($attrs); $existing->save(); $keepIds[] = (int) $existing->id; continue; } } $created = RubricaContattoCanale::query()->create($attrs); $keepIds[] = (int) $created->id; } $deleteQuery = RubricaContattoCanale::query()->where('rubrica_id', (int) $this->rubrica->id); if (! empty($keepIds)) { $deleteQuery->whereNotIn('id', $keepIds); } $deleteQuery->delete(); }); $this->mount((int) $this->rubrica->id); Notification::make() ->title('Contatti aggiornati') ->success() ->send(); }), Action::make('riferimenti_chiamata') ->label('Riferimenti chiamata') ->icon('heroicon-o-phone-arrow-up-right') ->form([ Select::make('tipo_utenza_call') ->label('Profilo chiamante') ->native(false) ->options([ 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'fornitore' => 'Fornitore', 'amministratore' => 'Amministratore', 'altro' => 'Altro', ]), TextInput::make('riferimento_stabile') ->label('Riferimento stabile') ->maxLength(255), TextInput::make('riferimento_unita') ->label('Riferimento unità') ->maxLength(255), TextInput::make('note_segreteria') ->label('Note segreteria') ->maxLength(2000), ]) ->fillForm(fn(): array=> [ 'tipo_utenza_call' => $this->rubrica->tipo_utenza_call, 'riferimento_stabile' => $this->rubrica->riferimento_stabile, 'riferimento_unita' => $this->rubrica->riferimento_unita, 'note_segreteria' => $this->rubrica->note_segreteria, ]) ->action(function (array $data): void { $this->rubrica->tipo_utenza_call = ! empty($data['tipo_utenza_call']) ? (string) $data['tipo_utenza_call'] : null; $this->rubrica->riferimento_stabile = ! empty($data['riferimento_stabile']) ? (string) $data['riferimento_stabile'] : null; $this->rubrica->riferimento_unita = ! empty($data['riferimento_unita']) ? (string) $data['riferimento_unita'] : null; $this->rubrica->note_segreteria = ! empty($data['note_segreteria']) ? (string) $data['note_segreteria'] : null; $this->rubrica->data_ultima_modifica = now()->toDateString(); $this->rubrica->modificato_da = Auth::id(); $this->rubrica->save(); $this->mount((int) $this->rubrica->id); Notification::make() ->title('Riferimenti chiamata aggiornati') ->success() ->send(); }), Action::make('ruoli_legami') ->label('Ruoli / legami') ->icon('heroicon-o-link') ->modalWidth('7xl') ->form([ Repeater::make('ruoli') ->label('Ruoli e legami') ->defaultItems(0) ->reorderable(true) ->schema([ Hidden::make('id'), Select::make('ruolo_standard') ->label('Ruolo') ->options(self::getRuoliLabels()) ->required() ->native(false) ->default('altro') ->columnSpan(2), TextInput::make('ruolo_custom') ->label('Ruolo custom') ->maxLength(255) ->columnSpan(2), Select::make('stabile_id') ->label('Stabile') ->native(false) ->searchable() ->options(fn() => $this->getStabiliOptions()) ->reactive() ->afterStateUpdated(fn($set) => $set('unita_immobiliare_id', null)) ->columnSpan(3), Select::make('unita_immobiliare_id') ->label('Unità') ->native(false) ->searchable() ->options(fn($get) => $this->getUnitaOptions($get('stabile_id'))) ->columnSpan(3), DatePicker::make('data_inizio') ->label('Data inizio') ->columnSpan(2), DatePicker::make('data_fine') ->label('Data fine') ->columnSpan(2), Toggle::make('is_preferito') ->label('Preferito') ->default(false) ->columnSpan(2), Toggle::make('is_attivo') ->label('Attivo') ->default(true) ->columnSpan(2), ]) ->columns(12), ]) ->fillForm(function (): array { $ruoli = RubricaRuolo::query() ->where('rubrica_id', (int) $this->rubrica->id) ->orderByDesc('is_attivo') ->orderByDesc('is_preferito') ->orderByDesc('data_inizio') ->orderByDesc('id') ->limit(200) ->get(); return [ 'ruoli' => $ruoli->map(fn(RubricaRuolo $r) => [ 'id' => (int) $r->id, 'ruolo_standard' => (string) ($r->ruolo_standard ?? 'altro'), 'ruolo_custom' => (string) ($r->ruolo_custom ?? ''), 'stabile_id' => $r->stabile_id ? (int) $r->stabile_id : null, 'unita_immobiliare_id' => $r->unita_immobiliare_id ? (int) $r->unita_immobiliare_id : null, 'data_inizio' => $r->data_inizio?->format('Y-m-d'), 'data_fine' => $r->data_fine?->format('Y-m-d'), 'is_preferito' => (bool) ($r->is_preferito ?? false), 'is_attivo' => (bool) ($r->is_attivo ?? true), ])->all(), ]; }) ->action(function (array $data): void { /** @var array> $rows */ $rows = $data['ruoli'] ?? []; $rows = is_array($rows) ? array_values($rows) : []; DB::transaction(function () use ($rows): void { $keepIds = []; foreach ($rows as $row) { $id = (int) ($row['id'] ?? 0); $ruoloStandard = (string) ($row['ruolo_standard'] ?? 'altro'); $attrs = [ 'rubrica_id' => (int) $this->rubrica->id, 'ruolo_standard' => $ruoloStandard ?: 'altro', 'ruolo_custom' => ! empty($row['ruolo_custom']) ? (string) $row['ruolo_custom'] : null, 'stabile_id' => ! empty($row['stabile_id']) ? (int) $row['stabile_id'] : null, 'unita_immobiliare_id' => ! empty($row['unita_immobiliare_id']) ? (int) $row['unita_immobiliare_id'] : null, 'data_inizio' => $row['data_inizio'] ?? null, 'data_fine' => $row['data_fine'] ?? null, 'is_preferito' => (bool) ($row['is_preferito'] ?? false), 'is_attivo' => (bool) ($row['is_attivo'] ?? true), ]; if ($id > 0) { $existing = RubricaRuolo::query() ->where('id', $id) ->where('rubrica_id', (int) $this->rubrica->id) ->first(); if ($existing) { $existing->fill($attrs); $existing->save(); $keepIds[] = (int) $existing->id; continue; } } $created = RubricaRuolo::query()->create($attrs); $keepIds[] = (int) $created->id; } $deleteQuery = RubricaRuolo::query()->where('rubrica_id', (int) $this->rubrica->id); if (! empty($keepIds)) { $deleteQuery->whereNotIn('id', $keepIds); } $deleteQuery->delete(); }); $this->mount((int) $this->rubrica->id); Notification::make() ->title('Ruoli / legami aggiornati') ->success() ->send(); }), Action::make('torna') ->label('Torna') ->icon('heroicon-o-arrow-left') ->url(fn() => AnagraficaUnica::getUrl(panel: 'admin-filament')), ]; } private function fillInlineForm(): void { $legacyIdentity = $this->extractLegacyIdentityFromNote($this->rubrica->note ?? null); $resolvedAddress = $this->parseAddressComponents($this->rubrica->indirizzo_completo ?? null); $this->inlineForm = [ 'nome' => (string) ($this->rubrica->nome ?? ''), 'cognome' => (string) ($this->rubrica->cognome ?? ''), 'ragione_sociale' => (string) ($this->rubrica->ragione_sociale ?? ''), 'tipo_contatto' => (string) ($this->rubrica->tipo_contatto ?? ''), 'categoria' => (string) ($this->rubrica->categoria ?? ''), 'codice_fiscale' => (string) ($this->rubrica->codice_fiscale ?? ''), 'partita_iva' => (string) ($this->rubrica->partita_iva ?? ''), 'sesso' => (string) ($this->rubrica->sesso ?? $legacyIdentity['sesso'] ?? ''), 'data_nascita' => (string) (($this->rubrica->data_nascita?->format('Y-m-d')) ?? $legacyIdentity['data_nascita'] ?? ''), 'luogo_nascita' => (string) ($this->rubrica->luogo_nascita ?? $legacyIdentity['luogo_nascita'] ?? ''), 'provincia_nascita' => (string) ($this->rubrica->provincia_nascita ?? $legacyIdentity['provincia_nascita'] ?? ''), 'email' => (string) ($this->rubrica->email ?? ''), 'pec' => (string) ($this->rubrica->pec ?? ''), 'telefono_ufficio' => (string) ($this->rubrica->telefono_ufficio ?? ''), 'telefono_cellulare' => (string) ($this->rubrica->telefono_cellulare ?? ''), 'telefono_casa' => (string) ($this->rubrica->telefono_casa ?? ''), 'indirizzo' => (string) ($this->rubrica->indirizzo ?? $resolvedAddress['indirizzo'] ?? ''), 'civico' => (string) ($this->rubrica->civico ?? $resolvedAddress['civico'] ?? ''), 'cap' => (string) ($this->rubrica->cap ?? $resolvedAddress['cap'] ?? ''), 'citta' => (string) ($this->rubrica->citta ?? $resolvedAddress['citta'] ?? ''), 'provincia' => (string) ($this->rubrica->provincia ?? $resolvedAddress['provincia'] ?? ''), 'nazione' => (string) ($this->rubrica->nazione ?? 'Italia'), 'indirizzo_completo' => (string) ($this->rubrica->indirizzo_completo ?? ''), 'tipo_utenza_call' => (string) ($this->rubrica->tipo_utenza_call ?? ''), 'riferimento_stabile' => (string) ($this->rubrica->riferimento_stabile ?? ''), 'riferimento_unita' => (string) ($this->rubrica->riferimento_unita ?? ''), 'note' => (string) ($legacyIdentity['note_clean'] ?? $this->rubrica->note ?? ''), 'note_segreteria' => (string) ($this->rubrica->note_segreteria ?? ''), ]; } private function cleanNullable(mixed $value): mixed { if (! is_string($value)) { return $value; } $trim = trim($value); return $trim === '' ? null : $trim; } /** @return array */ private function extractLegacyIdentityFromNote(mixed $note): array { $raw = trim((string) ($note ?? '')); if ($raw === '') { return [ 'note_clean' => null, 'titolo_id' => null, 'sesso' => null, 'data_nascita' => null, 'luogo_nascita' => null, 'provincia_nascita' => null, ]; } $parsed = [ 'titolo_id' => null, 'sesso' => null, 'data_nascita' => null, 'luogo_nascita' => null, 'provincia_nascita' => null, ]; $cleanLines = []; foreach (preg_split('/\r\n|\r|\n/', $raw) ?: [] as $line) { $trimmed = trim($line); if (! str_starts_with($trimmed, 'Dati anagrafici legacy:')) { $cleanLines[] = $line; continue; } $payload = trim(substr($trimmed, strlen('Dati anagrafici legacy:'))); $bits = preg_split('/\s*\|\s*/', $payload) ?: []; foreach ($bits as $bit) { [$key, $value] = array_pad(explode(':', $bit, 2), 2, null); $key = mb_strtolower(trim((string) $key)); $value = trim((string) $value); if ($value === '') { continue; } if ($key === 'titolo') { $parsed['titolo_id'] = $this->resolveTitoloId($value); continue; } if ($key === 'sesso') { $parsed['sesso'] = $this->normalizeSessoValue($value); continue; } if ($key === 'data nascita') { $parsed['data_nascita'] = $this->normalizeLegacyDateValue($value); continue; } if ($key === 'luogo nascita') { $parsed['luogo_nascita'] = $value; continue; } if ($key === 'provincia nascita') { $parsed['provincia_nascita'] = $this->normalizeProvince($value); } } } $cleanLines = array_values(array_filter($cleanLines, fn($line) => trim((string) $line) !== '')); return $parsed + [ 'note_clean' => $cleanLines === [] ? null : implode("\n", $cleanLines), ]; } /** @return array */ private function parseAddressComponents(mixed $value): array { $full = trim((string) ($value ?? '')); if ($full === '') { return [ 'indirizzo' => null, 'civico' => null, 'cap' => null, 'citta' => null, 'provincia' => null, ]; } $parts = [ 'indirizzo' => $full, 'civico' => null, 'cap' => null, 'citta' => null, 'provincia' => null, ]; if (preg_match('/\((?[A-Za-z]{2})\)\s*$/', $full, $provinceMatch) === 1) { $parts['provincia'] = strtoupper($provinceMatch['provincia']); $full = trim(substr($full, 0, -strlen($provinceMatch[0]))); } if (preg_match('/\b(?\d{5})\b/', $full, $capMatch, PREG_OFFSET_CAPTURE) === 1) { $parts['cap'] = $capMatch['cap'][0]; $capPos = $capMatch['cap'][1]; $beforeCap = trim(substr($full, 0, $capPos)); $afterCap = trim(substr($full, $capPos + 5)); if ($afterCap !== '') { $parts['citta'] = $afterCap; } $full = $beforeCap; } if (preg_match('/^(?.*?)(?:,\s*|\s+)(?\d+[A-Za-z\/-]*)$/', $full, $streetMatch) === 1) { $parts['indirizzo'] = trim($streetMatch['indirizzo']) ?: null; $parts['civico'] = trim($streetMatch['civico']) ?: null; } else { $parts['indirizzo'] = $full; } return $parts; } private function normalizeLegacyDateValue(mixed $value): ?string { if ($value instanceof \DateTimeInterface) { return $value->format('Y-m-d'); } $raw = trim((string) ($value ?? '')); if ($raw === '') { return null; } foreach (['Y-m-d', 'd/m/Y', 'd-m-Y', 'd.m.Y', 'd/m/y', 'd-m-y', 'd.m.y', 'm/d/Y', 'm/d/y'] as $format) { $dt = \DateTime::createFromFormat($format, $raw); if ($dt instanceof \DateTime) { return $this->normalizeCenturyDrift($dt)->format('Y-m-d'); } } $ts = strtotime($raw); if ($ts === false) { return null; } return $this->normalizeCenturyDrift((new \DateTime())->setTimestamp($ts))->format('Y-m-d'); } private function normalizeCenturyDrift(\DateTime $date): \DateTime { $thresholdYear = (int) date('Y') + 1; if ((int) $date->format('Y') > $thresholdYear) { $date->modify('-100 years'); } return $date; } private function normalizeSessoValue(mixed $value): ?string { $raw = strtoupper(trim((string) ($value ?? ''))); if ($raw === '') { return null; } if ($raw === 'M' || str_starts_with($raw, 'MAS')) { return 'M'; } if ($raw === 'F' || str_starts_with($raw, 'FEM')) { return 'F'; } return null; } private function normalizeProvince(mixed $value): ?string { $raw = strtoupper(trim((string) ($value ?? ''))); if ($raw === '') { return null; } return substr($raw, 0, 2); } private function resolveTitoloId(?string $raw): ?int { $raw = trim((string) ($raw ?? '')); if ($raw === '' || ! Schema::hasTable('titoli')) { return null; } $key = mb_strtolower($raw); $title = Titolo::query() ->whereRaw('LOWER(sigla) = ?', [$key]) ->orWhereRaw('LOWER(nome) = ?', [$key]) ->first(); return $title?->id ? (int) $title->id : null; } private function loadEmailMultiple(): void { $persona = $this->resolvePersonaForRubrica(false); if (! $persona instanceof Persona) { $this->emailMultiple = []; return; } $this->emailMultiple = $persona->emailMultiple() ->orderByDesc('attiva') ->orderBy('tipo_email') ->orderBy('email') ->get() ->map(fn(PersonaEmailMultipla $row): array=> [ 'id' => (int) $row->id, 'email' => (string) $row->email, 'tipo_email' => (string) ($row->tipo_email ?? 'secondaria'), 'attiva' => (bool) $row->attiva, ]) ->all(); } /** @return array> */ private function getAdditionalEmailsFormRows(): array { return $this->emailMultiple; } /** @param array> $rows */ private function saveAdditionalEmails(array $rows): void { $persona = $this->resolvePersonaForRubrica(true); if (! $persona instanceof Persona) { throw new \RuntimeException('Impossibile associare la rubrica a una persona.'); } $this->syncPersonaFromRubrica($persona); DB::transaction(function () use ($rows, $persona): void { $keepIds = []; foreach (array_values($rows) as $row) { $email = mb_strtolower(trim((string) ($row['email'] ?? ''))); if ($email === '') { continue; } $attrs = [ 'email' => $email, 'tipo_email' => trim((string) ($row['tipo_email'] ?? 'secondaria')) ?: 'secondaria', 'attiva' => (bool) ($row['attiva'] ?? true), ]; $id = (int) ($row['id'] ?? 0); if ($id > 0) { $existing = PersonaEmailMultipla::query() ->where('persona_id', (int) $persona->id) ->where('id', $id) ->first(); if ($existing instanceof PersonaEmailMultipla) { $existing->fill($attrs); $existing->save(); $keepIds[] = (int) $existing->id; continue; } } $created = PersonaEmailMultipla::query()->create([ 'persona_id' => (int) $persona->id, ...$attrs, ]); $keepIds[] = (int) $created->id; } $deleteQuery = PersonaEmailMultipla::query()->where('persona_id', (int) $persona->id); if ($keepIds !== []) { $deleteQuery->whereNotIn('id', $keepIds); } $deleteQuery->delete(); }); $this->loadEmailMultiple(); } private function getStudioCollaboratoreEmail(): ?string { $primary = mb_strtolower(trim((string) ($this->rubrica->email ?? ''))); if ($primary !== '') { return $primary; } foreach ($this->emailMultiple as $row) { $email = mb_strtolower(trim((string) ($row['email'] ?? ''))); if ($email !== '' && (bool) ($row['attiva'] ?? true)) { return $email; } } return null; } public function canRequestClickToCall(): bool { $user = Auth::user(); return $user instanceof User && (bool) ($user->pbx_click_to_call_enabled ?? false) && trim((string) ($user->pbx_extension ?? '')) !== ''; } public function richiediClickToCallRubrica(string $field): void { if (! in_array($field, ['telefono_ufficio', 'telefono_cellulare', 'telefono_casa'], true)) { Notification::make()->title('Campo telefono non supportato')->warning()->send(); return; } $user = Auth::user(); if (! $user instanceof User) { return; } $extension = trim((string) ($user->pbx_extension ?? '')); if (! (bool) ($user->pbx_click_to_call_enabled ?? false) || $extension === '') { Notification::make()->title('Click-to-call non abilitato per il tuo utente')->warning()->send(); return; } $phone = $this->normalizePhoneDigits(data_get($this->rubrica, $field)); if ($phone === '') { Notification::make()->title('Numero non valido')->warning()->send(); return; } PbxClickToCallRequest::query()->create([ 'requested_by_user_id' => (int) $user->id, 'assigned_user_id' => (int) $user->id, 'stabile_id' => $this->resolveClickToCallStabileId($user), 'source_extension' => $extension, 'target_number' => $phone, 'status' => 'pending', 'note' => 'Richiesta da scheda rubrica', 'requested_at' => now(), 'metadata' => [ 'requested_from' => 'rubrica_universale', 'rubrica_id' => (int) $this->rubrica->id, 'field' => $field, ], ]); Notification::make() ->title('Richiesta click-to-call registrata') ->body('Interno ' . $extension . ' -> ' . $phone) ->success() ->send(); } private function resolvePersonaForRubrica(bool $createIfMissing): ?Persona { if (! Schema::hasTable('persone')) { return null; } $cf = trim((string) ($this->rubrica->codice_fiscale ?? '')); if ($cf !== '') { $found = Persona::query()->where('codice_fiscale', $cf)->first(); if ($found instanceof Persona) { return $found; } } $email = mb_strtolower(trim((string) ($this->rubrica->email ?? ''))); if ($email !== '') { $found = Persona::query()->whereRaw('LOWER(email_principale) = ?', [$email])->first(); if ($found instanceof Persona) { return $found; } if (Schema::hasTable('persone_email_multiple')) { $match = Persona::query() ->whereHas('emailMultiple', fn(EloquentBuilderContract $query) => $query->where('email', $email)) ->first(); if ($match instanceof Persona) { return $match; } } } $phone = trim((string) ($this->rubrica->telefono_cellulare ?: $this->rubrica->telefono_ufficio ?: '')); if ($phone !== '') { $found = Persona::query()->where('telefono_principale', $phone)->first(); if ($found instanceof Persona) { return $found; } } $nome = trim((string) ($this->rubrica->nome ?? '')); $cognome = trim((string) ($this->rubrica->cognome ?? '')); if ($nome !== '' && $cognome !== '') { $found = Persona::query()->where('nome', $nome)->where('cognome', $cognome)->first(); if ($found instanceof Persona) { return $found; } } if (! $createIfMissing) { return null; } if ($nome === '' && $cognome === '' && trim((string) ($this->rubrica->ragione_sociale ?? '')) === '') { return null; } $persona = Persona::query()->create([ 'tipologia' => $this->rubrica->tipo_contatto === 'persona_giuridica' ? 'giuridica' : 'fisica', 'nome' => $nome !== '' ? $nome : (trim((string) ($this->rubrica->ragione_sociale ?? '')) !== '' ? 'Impresa' : 'N/D'), 'cognome' => $cognome !== '' ? $cognome : (trim((string) ($this->rubrica->ragione_sociale ?? '')) !== '' ? trim((string) $this->rubrica->ragione_sociale) : 'N/D'), 'ragione_sociale' => $this->rubrica->ragione_sociale, 'codice_fiscale' => $cf !== '' ? $cf : null, 'partita_iva' => $this->rubrica->partita_iva, 'data_nascita' => $this->rubrica->data_nascita, 'residenza_via' => $this->rubrica->indirizzo_completo, 'telefono_principale' => $phone !== '' ? $phone : null, 'telefono_secondario' => $this->rubrica->telefono_casa, 'email_principale' => $email !== '' ? $email : null, 'email_pec' => $this->rubrica->pec, 'note' => $this->rubrica->note, 'attivo' => $this->rubrica->stato !== 'inattivo', ]); return $persona; } private function syncPersonaFromRubrica(Persona $persona): void { $updates = []; if (! $persona->email_principale && $this->rubrica->email) { $updates['email_principale'] = mb_strtolower(trim((string) $this->rubrica->email)); } if (! $persona->email_pec && $this->rubrica->pec) { $updates['email_pec'] = $this->rubrica->pec; } if (! $persona->telefono_principale && ($this->rubrica->telefono_cellulare || $this->rubrica->telefono_ufficio)) { $updates['telefono_principale'] = $this->rubrica->telefono_cellulare ?: $this->rubrica->telefono_ufficio; } if (! $persona->residenza_via && $this->rubrica->indirizzo_completo) { $updates['residenza_via'] = $this->rubrica->indirizzo_completo; } if (! $persona->data_nascita && $this->rubrica->data_nascita) { $updates['data_nascita'] = $this->rubrica->data_nascita; } if (! $persona->codice_fiscale && $this->rubrica->codice_fiscale) { $updates['codice_fiscale'] = $this->rubrica->codice_fiscale; } if (! $persona->partita_iva && $this->rubrica->partita_iva) { $updates['partita_iva'] = $this->rubrica->partita_iva; } if ($updates !== []) { $persona->fill($updates); $persona->save(); } } public function getUrlUnita(?int $unitaId): ?string { if (! $unitaId) { return null; } if (! class_exists(\App\Filament\Pages\UnitaImmobiliarePage::class)) { return null; } return \App\Filament\Pages\UnitaImmobiliarePage::getUrl(panel: 'admin-filament') . '?unita_id=' . (int) $unitaId . '&back=' . urlencode((string) request()->getRequestUri()); } /** @return array */ public static function getRuoliLabels(): array { return [ 'super_admin' => 'Super admin', 'amministratore' => 'Amministratore', 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'fornitore' => 'Fornitore', 'manutentore' => 'Manutentore', 'banca' => 'Banca', 'assicurazione' => 'Assicurazione', 'collaboratore' => 'Collaboratore', 'tester' => 'Tester', 'ospite' => 'Ospite', 'altro' => 'Altro', ]; } /** @return array */ public function getStabiliOptions(): array { $query = Stabile::query(); $user = Auth::user(); $isSuperAdmin = $user instanceof User && method_exists($user, 'hasRole') ? (bool) $user->hasRole('super-admin') : false; if (! $isSuperAdmin && $this->adminId > 0) { $query->where('amministratore_id', $this->adminId); } return $query ->orderBy('codice_stabile') ->limit(200) ->get(['id', 'codice_stabile', 'denominazione']) ->mapWithKeys(fn(Stabile $s) => [ (int) $s->id => trim((string) ($s->codice_operatore ?? $s->codice_stabile ?? '') . ' - ' . (string) ($s->denominazione ?? 'Stabile')), ]) ->all(); } /** @return array */ protected function getTitoliOptions(): array { if (! Schema::hasTable('titoli')) { return []; } return Titolo::query() ->orderBy('ordine') ->orderBy('nome') ->get(['id', 'nome', 'sigla']) ->mapWithKeys(fn(Titolo $t) => [ (int) $t->id => trim((string) ($t->sigla ?: $t->nome)), ]) ->all(); } /** @return array */ public function getUnitaOptions($stabileId): array { $stabileId = (int) ($stabileId ?? 0); if ($stabileId <= 0) { return []; } $query = UnitaImmobiliare::query()->where('stabile_id', $stabileId); return $query ->orderBy('palazzina_id') ->orderBy('scala') ->orderBy('piano') ->orderBy('interno') ->orderBy('id') ->limit(1000) ->get(['id', 'codice_unita', 'denominazione', 'scala', 'piano', 'interno']) ->mapWithKeys(function (UnitaImmobiliare $u) { $codice = $u->codice_unita ?: ('ID ' . $u->id); $nome = $u->denominazione ?: 'Unità'; $pos = trim('Scala ' . ($u->scala ?? '-') . ' · Piano ' . ($u->piano ?? '-') . ' · Int. ' . ($u->interno ?? '-')); return [(int) $u->id => trim($codice . ' — ' . $nome . ' (' . $pos . ')')]; }) ->all(); } private function hydrateFornitoriWorkspace(): void { $rubricaId = (int) $this->rubrica->id; $cf = trim((string) ($this->rubrica->codice_fiscale ?? '')); $piva = trim((string) ($this->rubrica->partita_iva ?? '')); $email = mb_strtolower(trim((string) ($this->rubrica->email ?? ''))); $query = Fornitore::query(); if ($this->adminId > 0) { $query->where('amministratore_id', $this->adminId); } $query->where(function (Builder $q) use ($rubricaId, $cf, $piva, $email): void { $q->where('rubrica_id', $rubricaId); if ($cf !== '') { $q->orWhere('codice_fiscale', $cf); } if ($piva !== '') { $q->orWhere('partita_iva', $piva); } if ($email !== '') { $q->orWhereRaw('LOWER(email) = ?', [$email]); } }); $fornitori = $query ->orderByRaw("COALESCE(ragione_sociale, '')") ->orderBy('id') ->limit(80) ->get(['id', 'rubrica_id', 'ragione_sociale', 'nome', 'cognome', 'email', 'telefono', 'cellulare', 'codice_fiscale', 'partita_iva']); $this->fornitoriCollegati = $fornitori->map(fn(Fornitore $f) => [ 'id' => (int) $f->id, 'nome' => (string) ($f->ragione_sociale ?: trim((string) ($f->nome ?? '') . ' ' . (string) ($f->cognome ?? '')) ?: ('Fornitore #' . $f->id)), 'rubrica_id' => (int) ($f->rubrica_id ?? 0), 'email' => (string) ($f->email ?? ''), 'telefono' => (string) ($f->telefono ?: $f->cellulare ?: ''), 'cf' => (string) ($f->codice_fiscale ?? ''), 'piva' => (string) ($f->partita_iva ?? ''), ])->all(); $fornitoreIds = $fornitori->pluck('id')->map(fn($v) => (int) $v)->all(); if ($fornitoreIds === []) { $this->dipendentiFornitoreRows = []; return; } $dipendenti = FornitoreDipendente::query() ->with(['user.roles', 'fornitore']) ->whereIn('fornitore_id', $fornitoreIds) ->orderByDesc('attivo') ->orderBy('cognome') ->orderBy('nome') ->limit(300) ->get(); $this->dipendentiFornitoreRows = $dipendenti->map(function (FornitoreDipendente $d): array { $user = $d->user; $this->dipendentePbxExtension[(int) $d->id] = (string) ($user?->pbx_extension ?? ''); return [ 'id' => (int) $d->id, 'fornitore_id' => (int) $d->fornitore_id, 'fornitore_nome' => (string) ($d->fornitore?->ragione_sociale ?: ('Fornitore #' . $d->fornitore_id)), 'nome' => (string) $d->nome_completo, 'email' => (string) ($d->email ?? ''), 'telefono' => (string) ($d->telefono ?? ''), 'attivo' => (bool) $d->attivo, 'user_id' => $user ? (int) $user->id : null, 'ruoli' => $user ? $user->roles->pluck('name')->values()->all() : [], 'pbx_extension' => (string) ($user?->pbx_extension ?? ''), ]; })->all(); } public function collegaRubricaDipendente(int $fornitoreId): void { $rubricaId = (int) ($this->dipendenteRubricaSelect[$fornitoreId] ?? 0); if ($rubricaId <= 0) { Notification::make()->title('Inserisci ID rubrica da collegare')->warning()->send(); return; } $fornitore = Fornitore::query()->find($fornitoreId); if (! $fornitore || ($this->adminId > 0 && (int) ($fornitore->amministratore_id ?? 0) !== $this->adminId)) { Notification::make()->title('Fornitore non valido')->danger()->send(); return; } $rubricaDip = RubricaUniversale::query()->find($rubricaId); if (! $rubricaDip) { Notification::make()->title('Rubrica dipendente non trovata')->danger()->send(); return; } $email = mb_strtolower(trim((string) ($rubricaDip->email ?? ''))); $query = FornitoreDipendente::query()->where('fornitore_id', (int) $fornitore->id); if ($email !== '') { $query->whereRaw('LOWER(email) = ?', [$email]); } else { $query->where('nome', (string) ($rubricaDip->nome ?? '')) ->where('cognome', (string) ($rubricaDip->cognome ?? '')); } $dip = $query->first(); if (! $dip) { $dip = new FornitoreDipendente(); $dip->fornitore_id = (int) $fornitore->id; $dip->created_by_user_id = Auth::id(); } $dip->nome = (string) ($rubricaDip->nome ?: $rubricaDip->ragione_sociale ?: 'Dipendente'); $dip->cognome = (string) ($rubricaDip->cognome ?? ''); $dip->email = $email !== '' ? $email : null; $dip->telefono = (string) ($rubricaDip->telefono_cellulare ?: $rubricaDip->telefono_ufficio ?: ''); $dip->attivo = true; $dip->updated_by_user_id = Auth::id(); $dip->save(); unset($this->dipendenteRubricaSelect[$fornitoreId]); $this->hydrateFornitoriWorkspace(); Notification::make()->title('Dipendente collegato da rubrica')->success()->send(); } public function creaDipendenteManuale(int $fornitoreId): void { $draft = (array) ($this->dipendenteManualDraft[$fornitoreId] ?? []); $nome = trim((string) ($draft['nome'] ?? '')); $cognome = trim((string) ($draft['cognome'] ?? '')); $email = mb_strtolower(trim((string) ($draft['email'] ?? ''))); $telefono = trim((string) ($draft['telefono'] ?? '')); if ($nome === '') { Notification::make()->title('Nome dipendente obbligatorio')->warning()->send(); return; } $fornitore = Fornitore::query()->find($fornitoreId); if (! $fornitore || ($this->adminId > 0 && (int) ($fornitore->amministratore_id ?? 0) !== $this->adminId)) { Notification::make()->title('Fornitore non valido')->danger()->send(); return; } $dip = new FornitoreDipendente(); $dip->fornitore_id = (int) $fornitore->id; $dip->nome = $nome; $dip->cognome = $cognome !== '' ? $cognome : null; $dip->email = $email !== '' ? $email : null; $dip->telefono = $telefono !== '' ? $telefono : null; $dip->attivo = true; $dip->created_by_user_id = Auth::id(); $dip->updated_by_user_id = Auth::id(); $dip->save(); unset($this->dipendenteManualDraft[$fornitoreId]); $this->hydrateFornitoriWorkspace(); Notification::make()->title('Nuovo dipendente creato')->success()->send(); } public function abilitaAccessoDipendente(int $dipendenteId): void { $dip = FornitoreDipendente::query()->with('fornitore')->find($dipendenteId); if (! $dip || ($this->adminId > 0 && (int) ($dip->fornitore?->amministratore_id ?? 0) !== $this->adminId)) { Notification::make()->title('Dipendente non valido')->danger()->send(); return; } $email = mb_strtolower(trim((string) ($dip->email ?? ''))); if ($email === '') { Notification::make()->title('Email dipendente mancante')->warning()->send(); return; } $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first(); if (! $user) { $tmpPwd = Str::random(12); $user = User::query()->create([ 'name' => trim((string) ($dip->nome_completo ?: 'Utente fornitore')), 'email' => $email, 'password' => Hash::make($tmpPwd), 'email_verified_at' => now(), 'is_active' => true, ]); } $user->assignRole('fornitore'); $dip->user_id = (int) $user->id; $dip->updated_by_user_id = Auth::id(); $dip->save(); $this->hydrateFornitoriWorkspace(); Notification::make()->title('Accesso dipendente abilitato')->success()->send(); } public function salvaInternoDipendente(int $dipendenteId): void { $dip = FornitoreDipendente::query()->with('user', 'fornitore')->find($dipendenteId); if (! $dip || ! $dip->user || ($this->adminId > 0 && (int) ($dip->fornitore?->amministratore_id ?? 0) !== $this->adminId)) { Notification::make()->title('Dipendente/utente non valido')->danger()->send(); return; } if (! Schema::hasColumn('users', 'pbx_extension')) { Notification::make()->title('Campo interno PBX non disponibile')->warning()->send(); return; } $ext = trim((string) ($this->dipendentePbxExtension[$dipendenteId] ?? '')); $dip->user->pbx_extension = $ext !== '' ? $ext : null; $dip->user->save(); $this->hydrateFornitoriWorkspace(); Notification::make()->title('Interno PBX aggiornato')->success()->send(); } public function abilitaAccessoCollaboratoreStudio(): void { $email = $this->getStudioCollaboratoreEmail(); if ($email === null) { Notification::make() ->title('Email rubrica mancante') ->warning() ->body('Per creare o collegare il collaboratore di studio serve un indirizzo email principale o aggiuntivo attivo nella rubrica.') ->send(); return; } $name = trim((string) ($this->rubrica->nome_completo ?: $this->rubrica->ragione_sociale ?: 'Collaboratore Studio')); $user = User::query()->whereRaw('LOWER(email) = ?', [$email])->first(); if (! $user) { $tmpPwd = Str::random(12); $user = User::query()->create([ 'name' => $name, 'email' => $email, 'password' => Hash::make($tmpPwd), 'email_verified_at' => now(), 'is_active' => true, 'registration_status' => 'approved', ]); $this->studioCollaboratoreGeneratedPassword = $tmpPwd; } $user->assignRole('collaboratore'); $extension = trim($this->studioCollaboratorePbxExtension); if (Schema::hasColumn('users', 'pbx_extension')) { $user->pbx_extension = $extension !== '' ? $extension : null; $user->save(); } $this->syncStudioCollaboratoreStabili($user); $this->hydrateStudioCollaboratoreWorkspace(); Notification::make() ->title('Collaboratore di studio abilitato') ->success() ->body('Utente collegato alla rubrica e pronto per la gestione centralino/stabili.') ->send(); } public function salvaCollaboratoreStudio(): void { $userId = (int) ($this->studioCollaboratoreUserId ?? 0); if ($userId <= 0) { Notification::make() ->title('Collaboratore non ancora abilitato') ->warning() ->body('Crea o collega prima l\'utente collaboratore da questa rubrica.') ->send(); return; } $user = User::query()->find($userId); if (! $user) { Notification::make()->title('Utente collaboratore non trovato')->danger()->send(); return; } if (Schema::hasColumn('users', 'pbx_extension')) { $extension = trim($this->studioCollaboratorePbxExtension); $user->pbx_extension = $extension !== '' ? $extension : null; $user->save(); } $this->syncStudioCollaboratoreStabili($user); $this->hydrateStudioCollaboratoreWorkspace(); Notification::make() ->title('Collaboratore studio aggiornato') ->success() ->send(); } private function hydrateStudioCollaboratoreWorkspace(): void { $this->studioCollaboratoreUserId = null; $this->studioCollaboratoreUserEmail = $this->getStudioCollaboratoreEmail(); $this->studioCollaboratoreUserName = trim((string) ($this->rubrica->nome_completo ?: $this->rubrica->ragione_sociale ?: '')) ?: null; $this->studioCollaboratorePbxExtension = ''; $this->studioCollaboratoreRoles = []; $this->studioCollaboratoreGeneratedPassword = $this->studioCollaboratoreGeneratedPassword ?: null; $defaultStabili = array_values(array_unique(array_filter(array_map( fn(array $row): int => (int) ($row['id'] ?? 0), $this->stabili )))); if ($defaultStabili === [] && count($this->adminStabileIds) === 1) { $defaultStabili = $this->adminStabileIds; } $email = $this->studioCollaboratoreUserEmail; if ($email === null) { $this->studioCollaboratoreStabileIds = array_map('strval', $defaultStabili); return; } $user = User::query()->with('roles')->whereRaw('LOWER(email) = ?', [mb_strtolower($email)])->first(); if (! $user) { $this->studioCollaboratoreStabileIds = array_map('strval', $defaultStabili); return; } $this->studioCollaboratoreUserId = (int) $user->id; $this->studioCollaboratoreUserName = (string) $user->name; $this->studioCollaboratorePbxExtension = (string) ($user->pbx_extension ?? ''); $this->studioCollaboratoreRoles = $user->roles->pluck('name')->values()->all(); $assignedIds = method_exists($user, 'stabiliAssegnati') ? $user->stabiliAssegnati()->pluck('stabili.id')->map(fn($v) => (int) $v)->all() : []; $assignedForAdmin = ! empty($this->adminStabileIds) ? array_values(array_intersect($assignedIds, $this->adminStabileIds)) : $assignedIds; $this->studioCollaboratoreStabileIds = array_map('strval', $assignedForAdmin !== [] ? $assignedForAdmin : $defaultStabili); } private function syncStudioCollaboratoreStabili(User $user): void { if (! Schema::hasTable('collaboratore_stabile') || ! method_exists($user, 'stabiliAssegnati')) { return; } $requested = []; foreach ($this->studioCollaboratoreStabileIds as $value) { if (is_numeric($value)) { $requested[] = (int) $value; } } $requested = array_values(array_unique($requested)); if (! empty($this->adminStabileIds)) { $requested = array_values(array_intersect($requested, $this->adminStabileIds)); } $currentIds = $user->stabiliAssegnati()->pluck('stabili.id')->map(fn($v) => (int) $v)->all(); $outsideAdminIds = ! empty($this->adminStabileIds) ? array_values(array_diff($currentIds, $this->adminStabileIds)) : []; $user->stabiliAssegnati()->sync(array_values(array_unique(array_merge($outsideAdminIds, $requested)))); } private function normalizePhoneDigits(mixed $value): string { $digits = preg_replace('/\D+/', '', (string) ($value ?? '')); return is_string($digits) ? $digits : ''; } private function resolveClickToCallStabileId(User $user): ?int { $activeStabileId = (int) (StabileContext::resolveActiveStabileId($user) ?? 0); if ($activeStabileId > 0) { return $activeStabileId; } foreach ($this->stabili as $stabile) { $candidate = (int) ($stabile['id'] ?? 0); if ($candidate > 0) { return $candidate; } } return null; } }