fix(nominativi-affitti-adminer): canonical domain query for nominativi, modern affitti with unita/catasto/persona linkage and adminer integration
This commit is contained in:
parent
1ceac8e309
commit
2611d8f5a4
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user