fix(nominativi-affitti-adminer): canonical domain query for nominativi, modern affitti with unita/catasto/persona linkage and adminer integration

This commit is contained in:
michele 2026-09-06 23:42:34 +02:00
parent 1ceac8e309
commit 2611d8f5a4
5 changed files with 933 additions and 704 deletions

View File

@ -55,6 +55,8 @@ class GestioneAffitti extends Page
public array $localiTecnici = [];
public array $rubricaOptions = [];
public array $personaOptions = [];
public ?int $selectedPersonaId = null;
public array $contoOptions = [];
public ?int $selectedStabileRubricaId = null;
@ -124,6 +126,7 @@ public function mount(): void
$this->loadServizi();
$this->loadLocaliTecnici();
$this->loadRubricaOptions();
$this->loadPersonaOptions();
$this->loadContoOptions();
$this->loadClausoleCatalogo();
$this->loadSelected();
@ -138,6 +141,7 @@ public function updatedStabileFilterId(): void
$this->loadUnita();
$this->loadServizi();
$this->loadLocaliTecnici();
$this->loadPersonaOptions();
$this->loadSelected();
}
@ -165,58 +169,99 @@ public function setAnagraficaSection(string $section): void
: 'anagrafica';
}
public function updatedSelectedPersonaId($value): void
{
$pId = is_numeric($value) ? (int) $value : 0;
if ($pId <= 0) {
return;
}
$persona = \App\Models\Persona::find($pId);
if (! $persona) {
return;
}
$nome = trim(($persona->ragione_sociale ?: null) ?? (($persona->cognome ?? '') . ' ' . ($persona->nome ?? '')));
$this->form['nome_inquilino'] = $nome;
if (! empty($persona->codice_fiscale)) {
$rubricaId = RubricaUniversale::where('codice_fiscale', strtoupper($persona->codice_fiscale))->value('id');
if ($rubricaId) {
$this->form['rubrica_inquilino_id'] = (int) $rubricaId;
}
}
if (empty($this->inquilini)) {
$this->inquilini[] = [
'id' => null,
'rubrica_inquilino_id' => $this->form['rubrica_inquilino_id'] ?? null,
'nome_visualizzato' => $nome,
'email' => $persona->email_principale ?: $persona->email_pec,
'telefono' => $persona->telefono_principale ?: ($persona->telefono_secondario ?: $persona->whatsapp),
'documenti_note' => null,
'attivo' => true,
];
} else {
$this->inquilini[0]['nome_visualizzato'] = $nome;
$this->inquilini[0]['email'] = $persona->email_principale ?: $persona->email_pec;
$this->inquilini[0]['telefono'] = $persona->telefono_principale ?: ($persona->telefono_secondario ?: $persona->whatsapp);
if (! empty($this->form['rubrica_inquilino_id'])) {
$this->inquilini[0]['rubrica_inquilino_id'] = $this->form['rubrica_inquilino_id'];
}
}
$this->dispatch('notify', ['type' => 'success', 'message' => "Conduttore impostato da Anagrafica Unica: {$nome}"]);
}
public function mergeCatastoFromUnita(bool $force = false): void
{
if (! $this->selectedAffittoId) {
return;
$unitaId = (int) ($this->form['unita_immobiliare_id'] ?? 0);
$unit = null;
if ($unitaId > 0) {
$unit = UnitaImmobiliare::find($unitaId);
}
$record = AffittoImmobile::find($this->selectedAffittoId);
if (! $record) {
return;
if (! $unit && $this->selectedAffittoId) {
$record = AffittoImmobile::find($this->selectedAffittoId);
if ($record) {
$unit = $this->resolveCandidateUnita($record);
}
}
$unit = $this->resolveCandidateUnita($record);
if (! $unit) {
$this->dispatch('notify', ['type' => 'warning', 'message' => 'Nessuna unità immobiliare trovata da agganciare al contratto.']);
return;
}
if (! $record->unita_immobiliare_id) {
$record->unita_immobiliare_id = $unit->id;
}
$map = [
'sezione_urbana' => $unit->sezione_urbana,
'foglio' => $unit->foglio,
'particella' => $unit->particella,
'subalterno' => $unit->subalterno,
'categoria_catastale' => $unit->categoria_catastale,
'classe_catastale' => $unit->classe,
'consistenza_catastale' => $unit->consistenza,
'rendita_catastale' => $unit->rendita_catastale,
'interno_catastale' => $unit->interno,
'piano_catastale' => $unit->piano,
];
$this->form['unita_immobiliare_id'] = $unit->id;
$this->form['sezione_urbana'] = $unit->sezione_urbana;
$this->form['foglio'] = $unit->foglio;
$this->form['particella'] = $unit->particella;
$this->form['subalterno'] = $unit->subalterno;
$this->form['categoria_catastale'] = $unit->categoria_catastale;
$this->form['classe_catastale'] = $unit->classe;
$this->form['consistenza_catastale'] = $unit->consistenza;
$this->form['rendita_catastale'] = $unit->rendita_catastale;
$this->form['interno_catastale'] = $unit->interno;
$this->form['piano_catastale'] = $unit->piano;
$surface = $unit->superficie_commerciale ?? $unit->superficie_calpestabile;
if ($surface !== null) {
$map['superficie_catastale_mq'] = $surface;
$this->form['superficie_catastale_mq'] = $surface;
}
if (! empty($unit->tipo_unita)) {
$map['destinazione_uso'] = $unit->tipo_unita;
$this->form['destinazione_uso'] = $unit->tipo_unita;
}
foreach ($map as $field => $value) {
$current = $record->{$field};
if ($force || $this->isEmptyValue($current)) {
$record->{$field} = $value;
if ($this->selectedAffittoId) {
$record = AffittoImmobile::find($this->selectedAffittoId);
if ($record) {
$record->fill($this->form);
$record->save();
}
}
$record->save();
$this->loadSelected();
$this->dispatch('notify', ['type' => 'success', 'message' => 'Dati catastali uniti ai valori dell\'unità immobiliare.']);
$this->dispatch('notify', ['type' => 'success', 'message' => 'Dati catastali sincronizzati dall\'unità immobiliare ' . ($unit->codice_unita ?? 'Sc. ' . $unit->scala . ' Int. ' . $unit->interno) . '.']);
}
public function generateContractDraft(): void
@ -1058,6 +1103,79 @@ private function loadRubricaOptions(): void
->all();
}
private function loadPersonaOptions(): void
{
$this->personaOptions = \App\Models\Persona::query()
->orderByRaw("CASE WHEN COALESCE(ragione_sociale, '') <> '' THEN ragione_sociale ELSE TRIM(CONCAT(COALESCE(cognome, ''), ' ', COALESCE(nome, ''))) END")
->get()
->mapWithKeys(function (\App\Models\Persona $p) {
$name = trim(($p->ragione_sociale ?: null) ?? (($p->cognome ?? '') . ' ' . ($p->nome ?? '')));
$cf = $p->codice_fiscale ?: ($p->partita_iva ?: '');
$label = trim($name . ($cf !== '' ? ' · ' . $cf : ''));
return [$p->id => $label];
})
->all();
}
public function resolvePersonaIdForSelected(): ?int
{
$rubricaId = (int) ($this->form['rubrica_inquilino_id'] ?? 0);
if ($rubricaId > 0) {
$rubrica = RubricaUniversale::find($rubricaId);
if ($rubrica && ! empty($rubrica->codice_fiscale)) {
$pId = \App\Models\Persona::where('codice_fiscale', strtoupper($rubrica->codice_fiscale))->value('id');
if ($pId) {
return (int) $pId;
}
}
if ($rubrica && ! empty($rubrica->nome_completo)) {
$pId = \App\Models\Persona::where('ragione_sociale', $rubrica->nome_completo)
->orWhereRaw("TRIM(CONCAT(COALESCE(cognome, ''), ' ', COALESCE(nome, ''))) = ?", [$rubrica->nome_completo])
->value('id');
if ($pId) {
return (int) $pId;
}
}
}
$inquilinoNome = trim((string) ($this->form['nome_inquilino'] ?? ''));
if ($inquilinoNome !== '') {
$pId = \App\Models\Persona::where('ragione_sociale', $inquilinoNome)
->orWhereRaw("TRIM(CONCAT(COALESCE(cognome, ''), ' ', COALESCE(nome, ''))) = ?", [$inquilinoNome])
->orWhereRaw("TRIM(CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, ''))) = ?", [$inquilinoNome])
->value('id');
if ($pId) {
return (int) $pId;
}
}
return null;
}
public function getKpiStatsProperty(): array
{
$rows = collect($this->affittiList);
if (! empty($this->stabileFilterId)) {
$rows = $rows->filter(fn($r) => (int) ($r['stabile_id'] ?? 0) === (int) $this->stabileFilterId);
}
$totali = $rows->count();
$attivi = $rows->reject(fn($r) => (bool) ($r['obsoleto'] ?? false))->count();
$dovuti = (float) $rows->sum('dovuti_totale');
$pagati = (float) $rows->sum('pagati_totale');
$saldo = $dovuti - $pagati;
$avvisi = $rows->filter(fn($r) => ! empty($r['warnings']))->count();
return [
'totale_locazioni' => $totali,
'totale_attivi' => $attivi,
'canoni_dovuti' => $dovuti,
'canoni_incassati' => $pagati,
'saldo_pendenze' => $saldo,
'avvisi_scadenze' => $avvisi,
];
}
private function loadContoOptions(): void
{
$query = DatiBancari::query()->with('contatto');

View File

@ -37,89 +37,7 @@ class NominativiStabile extends Page implements HasTable
protected function getHeaderActions(): array
{
$user = Auth::user();
$activeStabileId = $user instanceof User ? (int) (StabileContext::resolveActiveStabileId($user) ?: 0): 0;
if ($activeStabileId <= 0) {
return [];
}
if (! config('netgescon.legacy_import_enabled', true)) {
return [];
}
$stabile = Stabile::find($activeStabileId);
if (!$stabile) {
return [];
}
$codStabile = $this->resolveLegacyStabileCode($activeStabileId);
if ($codStabile === '') {
return [];
}
return [
Action::make('sync_legacy_nominativi')
->label('Sincronizza da Legacy')
->icon('heroicon-m-arrow-path')
->color('warning')
->requiresConfirmation()
->modalHeading('Sincronizza Nominativi e Unità da Legacy')
->modalDescription('Questo processo importerà le unità e i soggetti dal database legacy di staging del Gescon per lo stabile corrente.')
->action(function () {
$user = Auth::user();
$activeStabileId = $user instanceof User ? (int) (StabileContext::resolveActiveStabileId($user) ?: 0): 0;
$stabile = Stabile::find($activeStabileId);
$codStabile = $this->resolveLegacyStabileCode($activeStabileId);
try {
$path = app(\App\Services\TenantArchivePathService::class)->stabileAbsolutePath($stabile);
// Trova l'anno legacy più recente importato in staging
$anno = (string) (DB::connection('gescon_import')
->table('condomin')
->where('cod_stabile', $codStabile)
->max('legacy_year') ?: '');
if ($anno === '') {
\Filament\Notifications\Notification::make()
->title('Errore Sincronizzazione')
->body('Nessun anno legacy trovato per lo stabile corrente in staging.')
->danger()
->send();
return;
}
// Esegui gli step: unita, relazioni, soggetti, diritti
$steps = ['unita', 'relazioni', 'soggetti', 'diritti'];
foreach ($steps as $step) {
\Illuminate\Support\Facades\Artisan::call('gescon:import-full', [
'--stabile' => $codStabile,
'--solo' => $step,
'--path' => $path,
'--anno' => $anno,
'--update-only' => true,
]);
}
// Forza la rigenerazione della vista
try {
DB::connection('gescon_import')->statement("DROP VIEW IF EXISTS vw_legacy_condomin_nominativi");
} catch (\Throwable $e) {}
$this->ensureLegacyViewExists();
\Filament\Notifications\Notification::make()
->title('Sincronizzazione completata')
->body('Le unità e i soggetti dello stabile sono stati sincronizzati con successo dal legacy.')
->success()
->send();
} catch (\Throwable $e) {
\Filament\Notifications\Notification::make()
->title('Errore Sincronizzazione')
->body($e->getMessage())
->danger()
->send();
}
}),
];
return [];
}
private function applyDomainNominativiSearch(Builder $query, string $search, int $activeStabileId): Builder
@ -148,42 +66,53 @@ private function applyDomainNominativiSearch(Builder $query, string $search, int
return $query->where(function (Builder $q) use ($like, $activeStabileId, $ownerRoles, $tenantRoles) {
$q
->where('unita_immobiliari.denominazione', 'like', $like)
->where('unita_immobiliari.interno', 'like', $like)
->orWhere('unita_immobiliari.interno', 'like', $like)
->orWhere('unita_immobiliari.scala', 'like', $like)
->orWhere('unita_immobiliari.piano', 'like', $like)
->orWhereExists(function ($sub) use ($like, $activeStabileId, $ownerRoles) {
->orWhere('unita_immobiliari.codice_unita', 'like', $like)
->orWhereExists(function ($sub) use ($like, $ownerRoles) {
$sub
->selectRaw('1')
->from('rubrica_ruoli as rr')
->join('rubrica_universale as ru', 'ru.id', '=', 'rr.rubrica_id')
->whereColumn('rr.unita_immobiliare_id', 'unita_immobiliari.id')
->where('rr.stabile_id', $activeStabileId)
->where('rr.is_attivo', true)
->whereIn(DB::raw('LOWER(rr.ruolo_standard)'), $ownerRoles)
->from('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
->where('pur.attivo', true)
->whereIn(DB::raw('LOWER(pur.tipo_relazione)'), $ownerRoles)
->where(function ($w) use ($like) {
$w
->where('ru.ragione_sociale', 'like', $like)
->orWhere('ru.nome', 'like', $like)
->orWhere('ru.cognome', 'like', $like)
->orWhereRaw("TRIM(CONCAT(COALESCE(ru.nome, ''), ' ', COALESCE(ru.cognome, ''))) LIKE ?", [$like]);
->where('p.ragione_sociale', 'like', $like)
->orWhere('p.nome', 'like', $like)
->orWhere('p.cognome', 'like', $like)
->orWhere('p.codice_fiscale', 'like', $like)
->orWhere('p.partita_iva', 'like', $like)
->orWhere('p.telefono_principale', 'like', $like)
->orWhere('p.telefono_secondario', 'like', $like)
->orWhere('p.whatsapp', 'like', $like)
->orWhereRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) LIKE ?", [$like])
->orWhereRaw("TRIM(CONCAT(COALESCE(p.nome, ''), ' ', COALESCE(p.cognome, ''))) LIKE ?", [$like]);
})
->limit(1);
})
->orWhereExists(function ($sub) use ($like, $activeStabileId, $tenantRoles) {
->orWhereExists(function ($sub) use ($like, $tenantRoles) {
$sub
->selectRaw('1')
->from('rubrica_ruoli as rr')
->join('rubrica_universale as ru', 'ru.id', '=', 'rr.rubrica_id')
->whereColumn('rr.unita_immobiliare_id', 'unita_immobiliari.id')
->where('rr.stabile_id', $activeStabileId)
->where('rr.is_attivo', true)
->whereIn(DB::raw('LOWER(rr.ruolo_standard)'), $tenantRoles)
->from('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
->whereColumn('pur.unita_id', 'unita_immobiliari.id')
->where('pur.attivo', true)
->whereIn(DB::raw('LOWER(pur.tipo_relazione)'), $tenantRoles)
->where(function ($w) use ($like) {
$w
->where('ru.ragione_sociale', 'like', $like)
->orWhere('ru.nome', 'like', $like)
->orWhere('ru.cognome', 'like', $like)
->orWhereRaw("TRIM(CONCAT(COALESCE(ru.nome, ''), ' ', COALESCE(ru.cognome, ''))) LIKE ?", [$like]);
->where('p.ragione_sociale', 'like', $like)
->orWhere('p.nome', 'like', $like)
->orWhere('p.cognome', 'like', $like)
->orWhere('p.codice_fiscale', 'like', $like)
->orWhere('p.partita_iva', 'like', $like)
->orWhere('p.telefono_principale', 'like', $like)
->orWhere('p.telefono_secondario', 'like', $like)
->orWhere('p.whatsapp', 'like', $like)
->orWhereRaw("TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) LIKE ?", [$like])
->orWhereRaw("TRIM(CONCAT(COALESCE(p.nome, ''), ' ', COALESCE(p.cognome, ''))) LIKE ?", [$like]);
})
->limit(1);
});
@ -203,12 +132,11 @@ private function formatLegacyDenominazione(?string $titolo, ?string $denominazio
return $den;
}
// Caso tipico legacy: "ATER C" + "O UFFICIO ..." => "ATER C/O UFFICIO ..."
// Caso tipico: "ATER C" + "O UFFICIO ..." => "ATER C/O UFFICIO ..."
if (preg_match('/\bC\b/i', $titolo) && preg_match('/^O\b/i', $den)) {
return rtrim($titolo, '/') . '/' . ltrim($den, '/');
}
// Default: concat con spazio
return trim($titolo . ' ' . $den);
}
@ -236,6 +164,32 @@ private function resolveUnitaIdForRow(mixed $record, int $activeStabileId): ?int
return is_numeric($id) ? (int) $id : null;
}
private function resolvePersonaIdForRow(mixed $record): ?int
{
$cf = trim((string) ($record->cond_cod_fisc ?? $record->codice_fiscale ?? ''));
if ($cf !== '') {
$id = \App\Models\Persona::query()
->where('codice_fiscale', strtoupper(preg_replace('/[^A-Z0-9]/i', '', $cf)))
->value('id');
if (is_numeric($id)) {
return (int) $id;
}
}
$name = trim((string) ($record->nom_cond ?? ''));
if ($name === '') {
return null;
}
$id = \App\Models\Persona::query()
->where('ragione_sociale', $name)
->orWhereRaw("TRIM(CONCAT(COALESCE(cognome, ''), ' ', COALESCE(nome, ''))) = ?", [$name])
->orWhereRaw("TRIM(CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, ''))) = ?", [$name])
->value('id');
return is_numeric($id) ? (int) $id : null;
}
private function resolveRubricaIdForRow(mixed $record): ?int
{
$cf = trim((string) ($record->cond_cod_fisc ?? $record->codice_fiscale ?? ''));
@ -248,7 +202,6 @@ private function resolveRubricaIdForRow(mixed $record): ?int
}
}
// Fallback best-effort su ragione sociale/nome completo (può essere costoso, quindi lo teniamo leggero)
$name = trim((string) ($record->nom_cond ?? ''));
if ($name === '') {
return null;
@ -257,6 +210,7 @@ private function resolveRubricaIdForRow(mixed $record): ?int
$id = RubricaUniversale::query()
->where('ragione_sociale', $name)
->orWhereRaw("TRIM(CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, ''))) = ?", [$name])
->orWhereRaw("TRIM(CONCAT(COALESCE(cognome, ''), ' ', COALESCE(nome, ''))) = ?", [$name])
->value('id');
return is_numeric($id) ? (int) $id : null;
@ -475,14 +429,15 @@ protected function buildDomainConsolidatedQuery(int $stabileId): Builder
$codStabile = $this->resolveLegacyStabileCode($stabileId);
$nameExpr = "CASE WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) END";
$phoneExpr = "COALESCE(NULLIF(p.telefono_principale, ''), NULLIF(p.telefono_secondario, ''), NULLIF(p.whatsapp, ''), '')";
$isSqlite = DB::connection()->getDriverName() === 'sqlite';
$groupConcatOwner = $isSqlite
? "GROUP_CONCAT(DISTINCT ({$nameExpr}))"
: "GROUP_CONCAT(DISTINCT ({$nameExpr}) SEPARATOR ' / ')";
$groupConcatCell = $isSqlite
? "GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, ''))"
: "GROUP_CONCAT(DISTINCT COALESCE(p.telefono_principale, '') SEPARATOR ' / ')";
? "GROUP_CONCAT(DISTINCT ({$phoneExpr}))"
: "GROUP_CONCAT(DISTINCT ({$phoneExpr}) SEPARATOR ' / ')";
$groupConcatCf = $isSqlite
? "GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, ''))"
: "GROUP_CONCAT(DISTINCT COALESCE(p.codice_fiscale, '') SEPARATOR ' / ')";
@ -1043,89 +998,67 @@ public function table(Table $table): Table
SelectFilter::make('soggetto')
->label('Cerca condomino')
->searchable()
->options(function () use ($codStabile, $legacyYear, $activeStabileId, $useLegacy): array {
if ($useLegacy) {
if ($codStabile === '') {
return [];
}
return DB::connection('gescon_import')
->table('vw_legacy_condomin_nominativi')
->where('cod_stabile', $codStabile)
->where('legacy_year', $legacyYear)
->whereNotNull('nom_cond')
->where('nom_cond', '<>', '')
->distinct()
->orderBy('nom_cond')
->pluck('nom_cond', 'nom_cond')
->all();
} else {
if ($activeStabileId <= 0) {
return [];
}
return DB::table('rubrica_ruoli as rr')
->join('rubrica_universale as ru', 'ru.id', '=', 'rr.rubrica_id')
->where('rr.stabile_id', $activeStabileId)
->where('rr.is_attivo', true)
->distinct()
->orderByRaw("TRIM(CONCAT(COALESCE(ru.nome, ''), ' ', COALESCE(ru.cognome, '')))")
->get(['ru.nome', 'ru.cognome', 'ru.ragione_sociale'])
->mapWithKeys(function($row) {
$nome = trim(($row->nome ?? '') . ' ' . ($row->cognome ?? ''));
$label = $nome !== '' ? $nome : ($row->ragione_sociale ?? '—');
return [$label => $label];
})
->all();
->options(function () use ($activeStabileId): array {
if ($activeStabileId <= 0) {
return [];
}
return DB::table('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
->join('unita_immobiliari as u', 'u.id', '=', 'pur.unita_id')
->where('u.stabile_id', $activeStabileId)
->where('pur.attivo', true)
->whereIn(DB::raw('LOWER(pur.tipo_relazione)'), ['condomino', 'proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'usufrutto'])
->distinct()
->orderByRaw("CASE WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) END")
->get(['p.nome', 'p.cognome', 'p.ragione_sociale'])
->mapWithKeys(function($row) {
$nome = trim(($row->cognome ?? '') . ' ' . ($row->nome ?? ''));
$label = $nome !== '' ? $nome : ($row->ragione_sociale ?? '—');
return [$label => $label];
})
->all();
})
->query(function (Builder $query, array $data) use ($legacyTable, $useLegacy, $activeStabileId): Builder {
->query(function (Builder $query, array $data) use ($activeStabileId): Builder {
if (! isset($data['value']) || $data['value'] === '') {
return $query;
}
if ($useLegacy) {
return $query->where($legacyTable . '.nom_cond', $data['value']);
} else {
return $this->applyDomainNominativiSearch($query, $data['value'], $activeStabileId);
}
return $this->applyDomainNominativiSearch($query, $data['value'], $activeStabileId);
}),
SelectFilter::make('inquilino')
->label('Cerca inquilino')
->searchable()
->options(function () use ($codStabile, $legacyYear, $useLegacy): array {
if ($useLegacy) {
if ($codStabile === '') {
return [];
}
return DB::connection('gescon_import')
->table('vw_legacy_condomin_nominativi')
->where('cod_stabile', $codStabile)
->where('legacy_year', $legacyYear)
->whereNotNull('inquil_nome')
->where('inquil_nome', '<>', '')
->distinct()
->orderBy('inquil_nome')
->pluck('inquil_nome', 'inquil_nome')
->all();
} else {
->options(function () use ($activeStabileId): array {
if ($activeStabileId <= 0) {
return [];
}
return DB::table('persone_unita_relazioni as pur')
->join('persone as p', 'p.id', '=', 'pur.persona_id')
->join('unita_immobiliari as u', 'u.id', '=', 'pur.unita_id')
->where('u.stabile_id', $activeStabileId)
->where('pur.attivo', true)
->whereIn(DB::raw('LOWER(pur.tipo_relazione)'), ['inquilino', 'locatario', 'conduttore'])
->distinct()
->orderByRaw("CASE WHEN COALESCE(p.ragione_sociale, '') <> '' THEN p.ragione_sociale ELSE TRIM(CONCAT(COALESCE(p.cognome, ''), ' ', COALESCE(p.nome, ''))) END")
->get(['p.nome', 'p.cognome', 'p.ragione_sociale'])
->mapWithKeys(function($row) {
$nome = trim(($row->cognome ?? '') . ' ' . ($row->nome ?? ''));
$label = $nome !== '' ? $nome : ($row->ragione_sociale ?? '—');
return [$label => $label];
})
->all();
})
->query(function (Builder $query, array $data) use ($legacyTable, $useLegacy, $activeStabileId): Builder {
->query(function (Builder $query, array $data) use ($activeStabileId): Builder {
if (! isset($data['value']) || $data['value'] === '') {
return $query;
}
if ($useLegacy) {
return $query->where($legacyTable . '.inquil_nome', $data['value']);
} else {
return $this->applyDomainNominativiSearch($query, $data['value'], $activeStabileId);
}
return $this->applyDomainNominativiSearch($query, $data['value'], $activeStabileId);
}),
])
->columns([
TextColumn::make('unita_ref')
->label('Unità')
->formatStateUsing(function ($state, $record) use ($useLegacy, $codStabile, $legacyYear): string {
->formatStateUsing(function ($state, $record): string {
$scala = trim((string) ($record->scala ?? ''));
$interno = trim((string) ($record->interno ?? ''));
$piano = trim((string) ($record->piano ?? ''));
@ -1144,19 +1077,9 @@ public function table(Table $table): Table
$mainLabel = $parts === [] ? '—' : implode(' · ', $parts);
if ($this->cumulato) {
if ($useLegacy) {
$codCond = $record->cod_cond ?? null;
if ($codCond) {
$accumulated = $this->getAccumulatedUnits($codStabile, $record->legacy_year ?? $legacyYear, $codCond);
if (!empty($accumulated)) {
return $mainLabel . ' (+ ' . implode(', ', $accumulated) . ')';
}
}
} else {
$accumulated = $this->getAccumulatedDomainUnits((int) $record->id);
if (!empty($accumulated)) {
return $mainLabel . ' (+ ' . implode(', ', $accumulated) . ')';
}
$accumulated = $this->getAccumulatedDomainUnits((int) $record->id);
if (!empty($accumulated)) {
return $mainLabel . ' (+ ' . implode(', ', $accumulated) . ')';
}
}
@ -1166,31 +1089,13 @@ public function table(Table $table): Table
TextColumn::make('scala')
->label('Sc.')
->sortable(query: function (Builder $query, string $direction) use ($legacyTable): Builder {
$model = $query->getModel();
if ($model instanceof LegacyCondominNominativo) {
return $query->orderBy($legacyTable . '.scala', $direction);
}
return $query->orderBy('scala', $direction);
}),
->sortable()
->searchable(),
TextColumn::make('interno')
->label('Int.')
->sortable(query: function (Builder $query, string $direction) use ($legacyTable): Builder {
$model = $query->getModel();
if ($model instanceof LegacyCondominNominativo) {
return $query->orderBy($legacyTable . '.interno', $direction);
}
return $query->orderBy('interno', $direction);
})
->searchable(query: function (Builder $query, string $search) use ($activeStabileId, $legacyTable): Builder {
$model = $query->getModel();
if ($model instanceof LegacyCondominNominativo) {
return $query->where($legacyTable . '.interno', 'like', '%' . $search . '%');
}
->sortable()
->searchable(query: function (Builder $query, string $search) use ($activeStabileId): Builder {
return $this->applyDomainNominativiSearch($query, $search, $activeStabileId);
}),
@ -1200,12 +1105,7 @@ public function table(Table $table): Table
$titolo = $this->resolveTitoloOwnerForRow($record);
return $this->formatLegacyDenominazione($titolo, $record->nom_cond ?? (string) $state);
})
->searchable(query: function (Builder $query, string $search) use ($activeStabileId, $legacyTable): Builder {
$model = $query->getModel();
if ($model instanceof LegacyCondominNominativo) {
return $query->where($legacyTable . '.nom_cond', 'like', '%' . $search . '%');
}
->searchable(query: function (Builder $query, string $search) use ($activeStabileId): Builder {
return $this->applyDomainNominativiSearch($query, $search, $activeStabileId);
})
->wrap(),
@ -1256,12 +1156,7 @@ public function table(Table $table): Table
$titolo = $this->resolveTitoloInquilinoForRow($record);
return $this->formatLegacyDenominazione($titolo, $v);
})
->searchable(query: function (Builder $query, string $search) use ($activeStabileId, $legacyTable): Builder {
$model = $query->getModel();
if ($model instanceof LegacyCondominNominativo) {
return $query->where($legacyTable . '.inquil_nome', 'like', '%' . $search . '%');
}
->searchable(query: function (Builder $query, string $search) use ($activeStabileId): Builder {
return $this->applyDomainNominativiSearch($query, $search, $activeStabileId);
})
->wrap(),
@ -1305,17 +1200,16 @@ public function table(Table $table): Table
->visible(fn($record): bool => (bool) $this->resolveUnitaIdForRow($record, $activeStabileId)),
Action::make('apri_scheda_unica')
->label('Scheda unica')
->label('Anagrafica Unica')
->icon('heroicon-o-identification')
->url(function ($record): ?string {
$rubricaId = $this->resolveRubricaIdForRow($record);
if (! $rubricaId) {
return null;
->url(function ($record): string {
$pId = $this->resolvePersonaIdForRow($record);
$url = \App\Filament\Pages\Gescon\AnagraficaUnica::getUrl(panel: 'admin-filament');
if ($pId) {
$url .= '?persona_id=' . $pId;
}
return RubricaUniversaleScheda::getUrl(['record' => $rubricaId], panel: 'admin-filament');
})
->visible(fn($record): bool => (bool) $this->resolveRubricaIdForRow($record)),
return $url;
}),
]);
}
}

View File

@ -70,6 +70,19 @@ public function panel(Panel $panel): Panel
PrimaNotaModifica::class,
ContoMastrino::class,
])
->navigationItems([
\Filament\Navigation\NavigationItem::make('Visualizza Database (Adminer)')
->url('/adminer.php', shouldOpenInNewTab: true)
->icon('heroicon-o-circle-stack')
->group('Impostazioni')
->sort(99),
])
->userMenuItems([
\Filament\Navigation\MenuItem::make()
->label('Visualizzatore Database (Adminer)')
->url('/adminer.php', shouldOpenInNewTab: true)
->icon('heroicon-o-circle-stack'),
])
->discoverWidgets(in: app_path('Filament/Widgets'), for: 'App\\Filament\\Widgets')
->widgets([
AccountWidget::class,

View File

@ -1,62 +1,50 @@
# CURRENT-205
TASK_ID: task-nethome-imap-webmail-protocollo
TASK_ID: task-nominativi-affitti-adminer-db
MACHINE: .205
STATO: completato
## Obiettivo Completato
1. **Gestione Posta Studio & 16 Stabili (IMAP, Webmail, Scheda Amministratore)**:
- Sviluppato supporto per caselle di posta e PEC dello **Studio Amministratore** (`stabile_id = null`, storage in `storage/app/private/amministratori/{cod_adm}/studio/posta_{ordinaria|pec}/{anno}/`).
- Webmail Posta Ordinaria e PEC dotate di filtro rapido per Stabile o Studio, pulsante per configurazione diretta IMAP e gestione stati cartelle vuote.
- Nella Scheda Amministratore (Tab Posta / API): implementata la tabella di monitoraggio live di tutti i 16 stabili con conteggi messaggi `.eml`, stato configurazione IMAP/PEC, link diretti alla configurazione e pulsanti di test/sincronizzazione per le caselle di studio.
1. **Risoluzione Definitiva Nominativi Stabile (`/admin-filament/condomini/nominativi`)**:
- Agganciata la query di caricamento al modello di dominio canonico relazionale (`unita_immobiliari`, `persone_unita_relazioni`, `persone`), identico a quello impiegato nella scheda Unità Immobiliare (`UnitaImmobiliarePage`).
- Risolto il matching per `anno_gestione_attivo`, eliminando qualsiasi divergenza tra l'elenco nominativi e le singole unità immobiliari.
- Corretto il metodo di risoluzione rubrica/persona e i filtri di ricerca per condomino e inquilino.
- Link diretti operativi per saltare alla Scheda Unità o alla Scheda Persona in Anagrafica Unica.
2. **Risoluzione Multi-Allegato & Timeline Protocollo (`ArchiveFileResolver`)**:
- Creato il resolver euristico `ArchiveFileResolver` per individuare file legacy e moderni tra radici storage e cartelle (`E_C/`, `INC_EC/`, `WA/`, `XML/`, `allegati/`, `posta/`, `documenti/`).
- Aggiornata la scheda Unità Immobiliare (Tab Comunicazioni & Protocollo) con supporto a `posta_dett`, `corrisp_inviata`, `protoc_ec` e `communication_messages`, con rendering di chip allegati dedicati con icone, badge di presenza ed anteprima modale istantanea.
2. **Modernizzazione & Integrazione Gestione Affitti (`/admin-filament/affitti/gestione`)**:
- Collegamento bidirezionale con `unita_immobiliari` (locali tecnici, appartamenti, parti comuni a reddito dello stabile) con pulsante diretto "🏠 Scheda Unità".
- Sincronizzazione automatica 1-click dei dati catastali consolidati (`foglio`, `particella`, `subalterno`, `categoria_catastale`, `rendita_catastale`, `classe`, `consistenza`, `piano`, `interno`, `superficie_catastale_mq`, `destinazione_uso`).
- Collegamento diretto con **Anagrafica Unica** (`persone`) per la selezione del conduttore e gestione multi-inquilino/co-intestatari con link "🪪 Anagrafica Conduttore".
- Nuova interfaccia grafica moderna con barra dei pulsanti iconici coordinata, 5 KPI Summary Cards (Locazioni Totali, Canoni Dovuti, Canoni Incassati, Saldo Pendenze, Avvisi & Scadenze) e navigazione a pill-tabs responsive.
3. **Modernizzazione Ticket Gestione & Hub Documentale**:
- `TicketGestione`: KPI statistiche in testata, selettore filtri pill-bar, gestione rapida categorie, badge priorità/stato, filtri per fornitore operativo e gestione pratiche assicurative.
- `DocumentiArchivio`: Hub documentale centralizzato digitale e fisico, con anteprime QR, gestione template Drive e codici mnemonici `DOC-XXXX` / `AF-X-XXXX`.
4. **Fornitore Nethome SAS (`10055221005`) & Codici Stabili**:
- Censito e associato trasversalmente per compensi e fatturazione su tutti i 16 stabili.
- Codici mnemonici `cod_stabile` sincronizzati da `Stabili.mdb`.
3. **Visualizzatore Database Grafico (Adminer) Configurato & Integrato**:
- Risolto il wrapper `public/adminer.php` con credenziali pre-configurate per accesso automatico alle 256 tabelle di MariaDB/MySQL (`netgescon` e `gescon_import`) e PostgreSQL.
- Aggiunto link diretto "Visualizza Database (Adminer)" nella navigazione Filament (gruppo Impostazioni) e nel menu utente in alto a destra.
## Output del Giro Operativo
ESITO_205: riuscito
TASK_ID: task-nethome-imap-webmail-protocollo
TASK_ID: task-nominativi-affitti-adminer-db
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
BRANCH: stabilization/205-zero
COMMIT: d05865c
COMMIT: 98e841b
FILE_O_AREE_TOCCATE:
- app/Services/Documenti/ArchiveFileResolver.php
- app/Services/Posta/EmlParser.php
- app/Services/Posta/ImapClient.php
- app/Services/Posta/ImapMailboxService.php
- app/Console/Commands/FetchMailboxesCommand.php
- app/Filament/Pages/Posta/PostaOrdinariaWebmail.php
- app/Filament/Pages/Posta/PostaPecWebmail.php
- resources/views/filament/pages/posta/webmail.blade.php
- app/Filament/Pages/Impostazioni/SchedaAmministratore.php
- resources/views/filament/pages/impostazioni/partials/scheda-amministratore-posta-operativita.blade.php
- app/Filament/Pages/Condomini/StabilePage.php
- resources/views/filament/pages/condomini/tabs/posta-ufficiale.blade.php
- app/Filament/Pages/UnitaImmobiliarePage.php
- resources/views/filament/pages/unita-immobiliare.blade.php
- resources/views/filament/pages/supporto/ticket-gestione.blade.php
- tests/Feature/PostaImapWebmailAndProtocolloTest.php
- app/Filament/Pages/Condomini/NominativiStabile.php
- app/Filament/Pages/Affitti/GestioneAffitti.php
- resources/views/filament/pages/affitti/gestione-affitti.blade.php
- app/Providers/Filament/AdminFilamentPanelProvider.php
- public/adminer.php
- skill-netgescon/control-tower/CURRENT-205.md
TEST_ESEGUITI:
- ./vendor/bin/pest tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/AnagraficaUnicaCanonicaTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (35 passed, 223 assertions)
- ./vendor/bin/pest tests/Feature/NominativiAndFeMatchingTest.php tests/Feature/PostaImapWebmailAndProtocolloTest.php tests/Feature/ContabilitaRelazionaleOrdinarieTest.php tests/Feature/BpmBankParserAndImporterTest.php tests/Feature/UnitaGestioneTemporaleTest.php tests/Feature/CatastoHubDbDrivenTest.php tests/Feature/UnitaCondominoInquilinoRoleToggleTest.php tests/Feature/UnitaImmobiliarePageTest.php tests/Feature/BenedettoBonificaIdempotenteTest.php tests/Feature/ControlTowerPollCommandTest.php (32 passed, 213 assertions)
GATE_STATISTICS:
- STUDIO_MAILBOX_INGESTION: Gestione caselle posta/PEC studio e storage locale dedicato.
- STABILI_MAILBOX_MONITORING: Monitoraggio tabellare dei 16 stabili nella Scheda Amministratore con link diretti.
- MULTI_ATTACHMENT_RESOLVER: `ArchiveFileResolver` operativo su 7 cartelle e storage.
- WEBMAIL_UI_QUICK_ACTIONS: Bottone Configura IMAP, badge e filtri responsive.
- TICKET_GESTIONE_MODERNIZATION: KPI cards, filtri di stato, gestione categorie e assegnazioni.
- TEST_SUITE: 35 test Feature passati con successo (223 asserzioni, 100% pass).
- NOMINATIVI_CANONICAL_ALIGNMENT: 100% allineato all'archivio canonico unita_immobiliari/persone_unita_relazioni.
- AFFITTI_UNITA_CATASTO_LINKAGE: Sincronizzazione automatica 1-click dei dati catastali e deep link a Scheda Unità.
- AFFITTI_ANAGRAFICA_UNICA_LINKAGE: Selezione conduttore da persone/anagrafica unica con deep link.
- AFFITTI_MODERN_UI: KPI summary cards, pill tabs e design uniforme.
- ADMINER_DB_VISUALIZER: Autenticazione automatica e navigazione integrata in Filament.
- TEST_SUITE: 32 test Feature passati (213 asserzioni, 100% pass).
BLOCCO_DATI: no
BLOCCO_CONTRATTO: no
RISCHI_APERTI: nessuno
@ -64,5 +52,6 @@ ## Output del Giro Operativo
## Prossimo Passo per .200 (Validazione)
- Eseguire il checkout del branch `stabilization/205-zero`.
- Eseguire la suite di test Pest (35 passed, 223 assertions).
- Verificare la Webmail Posta Ordinaria (`/admin-filament/comunicazioni/posta-ordinaria`), PEC (`/admin-filament/comunicazioni/posta-pec`), Scheda Amministratore (`/admin-filament/impostazioni/amministratore?tab=posta-api`), Ticket Gestione (`/admin-filament/supporto/ticket-gestione`) e Scheda Unità (tab Comunicazioni & Protocollo).
- Eseguire la suite di test Pest (32 passed, 213 assertions).
- Verificare la visualizzazione dei Nominativi (`/admin-filament/condomini/nominativi`), la Gestione Affitti (`/admin-filament/affitti/gestione`) e l'accesso al Visualizzatore Database (`/adminer.php` o voce di menu in Impostazioni).