diff --git a/app/Filament/Pages/Affitti/GestioneAffitti.php b/app/Filament/Pages/Affitti/GestioneAffitti.php index dc0ef7a..db789d8 100755 --- a/app/Filament/Pages/Affitti/GestioneAffitti.php +++ b/app/Filament/Pages/Affitti/GestioneAffitti.php @@ -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'); diff --git a/app/Filament/Pages/Condomini/NominativiStabile.php b/app/Filament/Pages/Condomini/NominativiStabile.php index 3bcf763..c235d19 100755 --- a/app/Filament/Pages/Condomini/NominativiStabile.php +++ b/app/Filament/Pages/Condomini/NominativiStabile.php @@ -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; + }), ]); } } diff --git a/app/Providers/Filament/AdminFilamentPanelProvider.php b/app/Providers/Filament/AdminFilamentPanelProvider.php index a65dd1d..45ba4e1 100755 --- a/app/Providers/Filament/AdminFilamentPanelProvider.php +++ b/app/Providers/Filament/AdminFilamentPanelProvider.php @@ -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, diff --git a/resources/views/filament/pages/affitti/gestione-affitti.blade.php b/resources/views/filament/pages/affitti/gestione-affitti.blade.php index c871a40..c982a96 100755 --- a/resources/views/filament/pages/affitti/gestione-affitti.blade.php +++ b/resources/views/filament/pages/affitti/gestione-affitti.blade.php @@ -1,44 +1,195 @@ -
-
- - - @if($selectedAffittoId) - @if($selectedAffittoObsoleto) - - @else - +
+ {{-- BARRA PULSANTINI ICONICI COMPATTI UNIFICATA --}} +
+ + +
+ + + @if($selectedAffittoId) + @if($selectedAffittoObsoleto) + + @else + + @endif @endif - @endif - Scheda Stabile +
- @if(config('netgescon.legacy_import_enabled', true)) -
-
-
- - + {{-- KPI SUMMARY CARDS --}} + @php($kpi = $this->kpiStats) +
+
+
+ 🏢 Locazioni + Attive: {{ $kpi['totale_attivi'] }}
-
- +
{{ $kpi['totale_locazioni'] }}
+
Totale unità gestite
+
+ +
+
+ 💶 Canoni Dovuti + 📈 +
+
€ {{ number_format($kpi['canoni_dovuti'], 2, ',', '.') }}
+
Importo complessivo emesso
+
+ +
+
+ 💳 Canoni Incassati + 💰 +
+
€ {{ number_format($kpi['canoni_incassati'], 2, ',', '.') }}
+
Totale quote riscosse
+
+ +
+
+ ⚖️ Saldo Pendenze + {{ $kpi['saldo_pendenze'] > 0.01 ? '⚠️' : '✅' }} +
+
+ € {{ number_format($kpi['saldo_pendenze'], 2, ',', '.') }} +
+
+ {{ $kpi['saldo_pendenze'] > 0.01 ? 'Canoni arretrati residui' : 'Nessun arretrato' }}
- @if(!empty($legacyLastSyncMessage)) -
{{ $legacyLastSyncMessage }}
+ +
+
+ 🔔 Scadenze / Avvisi + {{ $kpi['avvisi_scadenze'] > 0 ? '🔴' : '⚪' }} +
+
+ {{ $kpi['avvisi_scadenze'] }} +
+
+ {{ $kpi['avvisi_scadenze'] > 0 ? 'Rinnovi o AdE urgenti' : 'Tutti i contratti in regola' }} +
+
+
+ + {{-- SELETTORE STABILE & IMMOBILE CORRENTE --}} +
+
+
🏢 Stabile:
+ + + @if(!empty($affittiOptions)) +
📑 Immobile:
+ + @endif +
+ + @if($selectedAffittoId) +
+ @if(!empty($form['unita_immobiliare_id'])) + + 🏠 + Scheda Unità + + @endif + @php($resolvedPersonaId = $this->resolvePersonaIdForSelected()) + @if($resolvedPersonaId) + + 🪪 + Anagrafica Conduttore + + @endif +
@endif
- @endif -
- + {{-- PILL TAB BAR --}} +
+ @php + $tabs = [ + 'elenco' => ['label' => 'Elenco Immobili a Reddito', 'icon' => '🏢'], + 'anagrafica' => ['label' => 'Anagrafica & Catasto Unità', 'icon' => '🏠'], + 'banca' => ['label' => 'Banca & Incassi', 'icon' => '🏦'], + 'scadenze' => ['label' => 'Contratto & Clausole', 'icon' => '📜'], + 'canoni' => ['label' => 'Canoni & Rate', 'icon' => '💶'], + 'documenti' => ['label' => 'Documenti & Allegati', 'icon' => '📁'], + ]; + @endphp + @foreach($tabs as $key => $tabData) + + @endforeach
+ {{-- TAB 1: ELENCO IMMOBILI A REDDITO --}} @if($tab === 'elenco') @php $rows = collect($affittiList); @@ -73,254 +224,326 @@ } @endphp -
-
-
- - -
- @if($forceAll) +
+
+
- - + + + + +
+
+ +
- @endif -
- -
-
Totale elementi: {{ $rows->count() }}
+
+ Totale contratti: {{ $rows->count() }} +
- - +
+ - - - - - - - - - - - + + + + + + + + + + + - + @forelse($rows as $row) - - - - - + + + + + + + + - - - - @empty - + + + @endforelse
CodiceDescrizione immobileProprietarioInquilinoStabileDovutiPagatiSaldoAvvisiStatoAzioniCodiceDescrizione ImmobileProprietario / EnteConduttore / InquilinoStabileDovutiIncassatiSaldoAvvisi & ScadenzeStatoAzioni
{{ $row['codice_appartamento'] ?? '—' }}{{ $row['descrizione_immobile'] ?? '—' }}{{ $row['proprietario_nome'] ?? '—' }}{{ $row['nome_inquilino'] ?? '—' }}
{{ $row['codice_appartamento'] ?? '—' }}{{ $row['descrizione_immobile'] ?? '—' }}{{ $row['proprietario_nome'] ?? '—' }}{{ $row['nome_inquilino'] ?? '—' }} -
{{ $row['stabile'] ?? '—' }}
-
{{ $row['stabile_nome'] ?? '' }}
+
{{ $row['stabile'] ?? '—' }}
+
{{ $row['stabile_nome'] ?? '' }}
+
€ {{ number_format((float)($row['dovuti_totale'] ?? 0), 2, ',', '.') }}€ {{ number_format((float)($row['pagati_totale'] ?? 0), 2, ',', '.') }} + € {{ number_format((float)($row['saldo_totale'] ?? 0), 2, ',', '.') }} € {{ number_format((float)($row['dovuti_totale'] ?? 0), 2, ',', '.') }}€ {{ number_format((float)($row['pagati_totale'] ?? 0), 2, ',', '.') }}€ {{ number_format((float)($row['saldo_totale'] ?? 0), 2, ',', '.') }} @if(!empty($row['warnings'])) @foreach($row['warnings'] as $warning) -
{{ $warning }}
+
+ ⚠️ + {{ $warning }} +
@endforeach @else - Nessun avviso + Regolare @endif
+ @if(!empty($row['obsoleto'])) - Obsoleto + Obsoleto @else - Attivo + Attivo @endif - +
Nessun affitto trovato.
+ Nessun immobile a reddito o locazione trovata per i criteri selezionati. +
+ {{-- TAB 2: ANAGRAFICA & CATASTO UNITA --}} @elseif($tab === 'anagrafica')
-
- +
+ + @if($anagraficaSection === 'catasto') +
+ + +
+ @endif
@if($anagraficaSection === 'anagrafica') -
-
-

Anagrafica immobile

+
+
+

Unità & Identificativi

- - + +
+ + @if(!empty($form['unita_immobiliare_id'])) + + 🏠 + Apri + + @endif +
- - + +
- - -
-
- - + +
- - + +
- - + +
- - -
-
- - -
-
- - + +
-
-

Stabile collegato

-
- {{ $selectedStabileCode ?? '—' }} - @if($selectedStabileName) - · {{ $selectedStabileName }} + +
+

Indirizzo & Stabile

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ {{ $selectedStabileCode ?? '—' }} {{ $selectedStabileName ? '· ' . $selectedStabileName : '' }} +
+
+
+
+
+ + @elseif($anagraficaSection === 'catasto') +
+
+

Dati Catasto Consolidati

+ Collegamento diretto ai dati dell'unità catastale +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + @else + {{-- SEZIONE INQUILINI & CONDUTTORI --}} +
+
+
+

Conduttore Principale & Anagrafica Unica

+

Sincronizzazione diretta con il database centrale delle persone

+
+ +
+ + {{-- SELETTORE RAPIDO ANAGRAFICA UNICA --}} +
+ +
+ + @php($currentPId = $this->resolvePersonaIdForSelected()) + @if($currentPId) + + 🪪 + Apri Scheda Persona + @endif
- @if(!empty($selectedStabileRubricaId)) - Apri rubrica stabile - @endif
-
- @elseif($anagraficaSection === 'catasto') -
-
- - -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- @else -
-
-

Inquilini / Affittuari (uno-a-molti)

- -
-
+ +
@forelse($inquilini as $i => $inq) -
+
- - @foreach($rubricaOptions as $id => $label) @@ -328,210 +551,180 @@
- - + +
- - + +
- - + +
- - + +
- +
@empty -
Nessun inquilino associato.
+
Nessun conduttore associato. Utilizza il pulsante in alto per aggiungere il primo conduttore.
@endforelse
@endif
+ {{-- TAB 3: BANCA & INCASSI --}} @elseif($tab === 'banca') -
-
-

Banca di riferimento

-
+
+
+

Coordinate Bancarie del Contratto

+
- -
- @foreach($contoOptions as $id => $label) @endforeach @if(!empty($form['conto_bancario_id'])) - Movimenti + + 🏦 + Movimenti + @endif
- - + +
- - + +
- - + +
- - + +
-
-

Altri riferimenti

-
+ +
+

Parametri Contabili di Riferimento

+
- - + +
- - + +
- - -
-
- - + +
+ + {{-- TAB 4: CONTRATTO & CLAUSOLE --}} @elseif($tab === 'scadenze') -
-
-

Dati contratto

-
+
+
+

Durata Contratto & Adempimenti Fiscali

+
- - + +
- - -
I canoni precedenti restano fuori dal gestionale operativo e dalla riconciliazione bancaria.
+ +
- - + +
- - + +
- - + +
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - + +
- - + + +
+
+ + +
+
+ +
-
-
-

Clausole e postille contratto

- + +
+
+

Bozza Contratto & Clausole

+
-
-
-
- - -
-
- -
-
-
- - + +
+
+ +
+ @if($contractDraftPreview) + + @endif
+
@forelse($clausoleContratto as $i => $c) -
-
- -
- - -
-
-
- - +
+
+ +
- - @@ -539,109 +732,131 @@
- - -
-
- - + +
- +
@empty -
Nessuna clausola associata al contratto.
+
Nessuna clausola personalizzata impostata.
@endforelse
+ {{-- TAB 5: CANONI & RATE --}} @elseif($tab === 'canoni') -
-
-

Canoni (dovuti / pagati)

+
+
+
+

Canoni e Scadenziario Rate

+
+ Dovuti: € {{ number_format($totali['dovuti'] ?? 0, 2, ',', '.') }} · + Incassati: € {{ number_format($totali['pagati'] ?? 0, 2, ',', '.') }} · + Saldo Residuo: € {{ number_format($totali['saldo'] ?? 0, 2, ',', '.') }} +
+
- Dovuti: € {{ number_format($totali['dovuti'] ?? 0, 2, ',', '.') }} · Pagati: € {{ number_format($totali['pagati'] ?? 0, 2, ',', '.') }} - - + +
-
- - + +
+
+ - + - + - - + + - + @forelse($canoniRighe as $r) @php $d = (float)($r['totale_dovuto'] ?? 0); $p = (float)($r['totale_pagato'] ?? 0); @endphp - - - + + - - - + - + @empty - + + + @endforelse
PeriodoPeriodo / Mese Emissione / ScadenzaStatoStato DovutoPagatoSaldo progressivoIncassatoSaldo Progressivo
{{ $r['mese_descrizione'] ?? ($r['mese'] ?? '') }} {{ $r['anno'] ?? '' }} +
{{ $r['mese_descrizione'] ?? ($r['mese'] ?? '') }} {{ $r['anno'] ?? '' }}
{{ $r['data_emissione'] ?? '—' }}
-
scad: {{ $r['data_scadenza'] ?? '—' }}
+
Scad: {{ $r['data_scadenza'] ?? '—' }}
+ @if(($r['stato'] ?? '') === 'scaduto') - Scaduto + Scaduto @elseif(($r['stato'] ?? '') === 'da_pagare') - Da pagare + Da pagare @elseif(($r['stato'] ?? '') === 'non_emesso') - Non emesso + Non emesso @else - In regola + In regola @endif € {{ number_format($d, 2, ',', '.') }} + € {{ number_format($d, 2, ',', '.') }}
€ {{ number_format($p, 2, ',', '.') }}
@if(!empty($r['data_pagamento'])) -
{{ $r['data_pagamento'] }}
+
{{ $r['data_pagamento'] }}
@endif
€ {{ number_format((float)($r['saldo_progressivo'] ?? 0), 2, ',', '.') }}€ {{ number_format((float)($r['saldo_progressivo'] ?? 0), 2, ',', '.') }}
Nessun canone disponibile.
Nessun canone generato per questo immobile.
-
- Saldo totale: € {{ number_format($totali['saldo'] ?? 0, 2, ',', '.') }} -
+ + {{-- TAB 6: DOCUMENTI & ALLEGATI --}} @elseif($tab === 'documenti') -
-
-

Documenti contratto e parti

-
- - + - - - + + +
-
-

Documenti collegati allo stabile

-

I documenti caricati (contratto, documenti parti, comunicazioni fine locazione) restano disponibili anche in scheda stabile.

- Apri documenti collegati + +
+

Archivio Documentale Stabile

+

I documenti caricati (contratti registrati, ricevute telematiche, comunicazioni di recesso) vengono archiviati nel fascicolo dello stabile.

+ + 📁 + Apri Documenti Collegati Stabile +
@endif diff --git a/skill-netgescon/control-tower/CURRENT-205.md b/skill-netgescon/control-tower/CURRENT-205.md index 3538abd..82a7a4c 100644 --- a/skill-netgescon/control-tower/CURRENT-205.md +++ b/skill-netgescon/control-tower/CURRENT-205.md @@ -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). +