netgescon-day0/app/Filament/Pages/Gescon/RubricaUniversaleScheda.php

2052 lines
84 KiB
PHP

<?php
namespace App\Filament\Pages\Gescon;
use App\Models\Fornitore;
use App\Models\FornitoreDipendente;
use App\Models\PbxClickToCallRequest;
use App\Models\Persona;
use App\Models\PersonaEmailMultipla;
use App\Models\RubricaContattoCanale;
use App\Models\RubricaRuolo;
use App\Models\RubricaUniversale;
use App\Models\Stabile;
use App\Models\Titolo;
use App\Models\UnitaImmobiliare;
use App\Models\User;
use App\Support\StabileContext;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Contracts\Database\Eloquent\Builder as EloquentBuilderContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
class RubricaUniversaleScheda extends Page
{
protected static ?string $title = 'Scheda anagrafica (Rubrica)';
protected static ?string $slug = 'gescon/anagrafica/rubrica/{record}';
protected static bool $shouldRegisterNavigation = false;
protected string $view = 'filament.pages.gescon.rubrica-universale-scheda';
public RubricaUniversale $rubrica;
/** @var array<int, array<string, mixed>> */
public array $fornitori = [];
/** @var array<int, array<string, mixed>> */
public array $stabili = [];
/** @var array<int, array<string, mixed>> */
public array $contattiPrincipali = [];
/** @var array<int, array<string, mixed>> */
public array $ruoliAttivi = [];
/** @var array<int, array<string, mixed>> */
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<int, int> */
public array $adminStabileIds = [];
public bool $isInlineEditing = false;
public string $sideTab = 'collegamenti';
/** @var array<int,array<string,mixed>> */
public array $fornitoriCollegati = [];
/** @var array<int,array<string,mixed>> */
public array $dipendentiFornitoreRows = [];
/** @var array<int,int|string> */
public array $dipendenteRubricaSelect = [];
/** @var array<int,array<string,string>> */
public array $dipendenteManualDraft = [];
/** @var array<int,string> */
public array $dipendentePbxExtension = [];
/** @var array<int,string> */
public array $studioCollaboratoreStabileIds = [];
/** @var array<int,string> */
public array $studioCollaboratoreStabileOptions = [];
public ?int $studioCollaboratoreUserId = null;
public ?string $studioCollaboratoreUserEmail = null;
public ?string $studioCollaboratoreUserName = null;
public ?string $studioCollaboratoreGeneratedPassword = null;
public string $studioCollaboratorePbxExtension = '';
/** @var array<int,string> */
public array $studioCollaboratoreRoles = [];
/** @var array<string,mixed> */
public array $inlineForm = [];
/** @var array<int, array<string, mixed>> */
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->hydrateFornitoriWorkspace();
$this->hydrateStudioCollaboratoreWorkspace();
}
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<int, array<string, mixed>> $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<int, array<string, mixed>> $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<string, mixed> */
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<string, ?string> */
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('/\((?<provincia>[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(?<cap>\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('/^(?<indirizzo>.*?)(?:,\s*|\s+)(?<civico>\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<int, array<string, mixed>> */
private function getAdditionalEmailsFormRows(): array
{
return $this->emailMultiple;
}
/** @param array<int, array<string, mixed>> $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<string, string> */
protected 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<int, string> */
protected 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<int, string> */
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<int, string> */
protected 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;
}
}