feat(unita-ui): compattezza anagrafiche, tabella recapiti multicanale dinamica e pulizia RDP

This commit is contained in:
michele 2026-09-02 13:13:35 +02:00
parent 8eddca0b7f
commit 6f20d81a67
4 changed files with 412 additions and 188 deletions

View File

@ -85,6 +85,29 @@ public function handle(): int
$this->info('Anagrafiche corrotte rimaste bonificate.'); $this->info('Anagrafiche corrotte rimaste bonificate.');
} }
// 3b. Pulizia comproprietari incrociati orfani su Unità 233 (0013-A-1)
$unit233 = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '1')->first();
if ($unit233) {
DB::table('unita_anagrafica_periodo')
->where('unita_immobiliare_id', $unit233->id)
->where('ruolo_occupazione', 'comproprietario')
->delete();
}
// 3c. Pulizia assegnazioni errate RDP S.R.L. (consentita solo su B-3 e B-4)
if (Schema::hasTable('unita_immobiliare_nominativi')) {
$unitB3B4Ids = UnitaImmobiliare::where('stabile_id', $stabile->id)
->where('scala', 'B')
->whereIn('interno', ['3', '4'])
->pluck('id')
->all();
DB::table('unita_immobiliare_nominativi')
->where('stabile_id', $stabile->id)
->where('nominativo', 'like', '%RDP%')
->whereNotIn('unita_immobiliare_id', $unitB3B4Ids)
->delete();
}
// 4. Bonifica e allineamento Unità NGGC 163 (ID 267) // 4. Bonifica e allineamento Unità NGGC 163 (ID 267)
$unit163 = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'NGGC')->where('interno', '163')->first(); $unit163 = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'NGGC')->where('interno', '163')->first();
if ($unit163) { if ($unit163) {

View File

@ -518,6 +518,200 @@ public function resolveHumanYearLabel(string $yearCode, string $codStabile): str
return 'Anno ' . (is_numeric($yearCode) ? (int) $yearCode : $yearCode); return 'Anno ' . (is_numeric($yearCode) ? (int) $yearCode : $yearCode);
} }
public function getRecapitiMulticanaleTableProperty(): array
{
if (! $this->unita) {
return [];
}
$tableRows = [];
// 1. Dati da Anagrafiche e UAP
$activeEntities = array_merge(
$this->relazioniPerTipo['proprietari'] ?? [],
$this->relazioniPerTipo['inquilini'] ?? []
);
foreach ($activeEntities as $entity) {
$nome = trim((string) ($entity['nome'] ?? ''));
$ruolo = (string) ($entity['tipo'] ?? ($entity['tipo_relazione'] ?? 'Condomino'));
if ($nome === '') {
continue;
}
$anagId = (int) ($entity['persona_id'] ?? ($entity['soggetto_id'] ?? 0));
$anag = $anagId > 0 ? DB::table('anagrafiche')->find($anagId) : null;
if (! $anag) {
$anag = DB::table('anagrafiche')
->where(function ($q) use ($nome, $entity) {
$q->where('cognome', 'like', '%' . $nome . '%')
->orWhere('nome', 'like', '%' . $nome . '%')
->orWhere('ragione_sociale', 'like', '%' . $nome . '%');
if (! empty($entity['codice_fiscale'])) {
$q->orWhere('codice_fiscale', trim((string) $entity['codice_fiscale']));
}
})
->first();
}
if ($anag) {
if (! empty($anag->email)) {
$tableRows[] = [
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => 'Principale',
'soggetto' => $nome,
'ruolo' => $ruolo,
'valore' => $anag->email,
'action_url' => 'mailto:' . $anag->email,
'is_principal' => true,
];
}
if (! empty($anag->pec)) {
$tableRows[] = [
'canale' => 'Email PEC',
'icon' => '🛡️',
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
'etichetta' => 'PEC Ufficiale',
'soggetto' => $nome,
'ruolo' => $ruolo,
'valore' => $anag->pec,
'action_url' => 'mailto:' . $anag->pec,
'is_principal' => false,
];
}
if (! empty($anag->telefono)) {
$tableRows[] = [
'canale' => 'Cellulare / Tel',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => 'Personale',
'soggetto' => $nome,
'ruolo' => $ruolo,
'valore' => $anag->telefono,
'action_url' => 'tel:' . $anag->telefono,
'is_principal' => false,
];
}
if (! empty($anag->indirizzo)) {
$indFull = trim($anag->indirizzo . ' ' . ($anag->cap ?? '') . ' ' . ($anag->citta ?? '') . ' ' . ($anag->provincia ? '(' . $anag->provincia . ')' : ''));
$tableRows[] = [
'canale' => 'Indirizzo',
'icon' => '📍',
'color' => 'bg-slate-50 text-slate-700 border-slate-200',
'etichetta' => 'Residenza / Sede',
'soggetto' => $nome,
'ruolo' => $ruolo,
'valore' => $indFull,
'action_url' => null,
'is_principal' => false,
];
}
}
}
// 2. Dati da Legacy condomin_mirror
$legacyRow = $this->getLegacyCondominRow();
if ($legacyRow && is_array($legacyRow->legacy_payload ?? null)) {
$p = $legacyRow->legacy_payload;
$condName = trim((string) ($legacyRow->nom_cond ?? 'Condomino'));
$inqName = trim((string) ($legacyRow->inquil_nome ?? ($legacyRow->inquilino ?? 'Inquilino')));
if (! empty($p['E_mail_condomino']) && ! $this->hasContactValue($tableRows, (string) $p['E_mail_condomino'])) {
$tableRows[] = [
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => 'Email Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'valore' => trim((string) $p['E_mail_condomino']),
'action_url' => 'mailto:' . trim((string) $p['E_mail_condomino']),
'is_principal' => count($tableRows) === 0,
];
}
if (! empty($p['PEC_condomino']) && ! $this->hasContactValue($tableRows, (string) $p['PEC_condomino'])) {
$tableRows[] = [
'canale' => 'Email PEC',
'icon' => '🛡️',
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
'etichetta' => 'PEC Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'valore' => trim((string) $p['PEC_condomino']),
'action_url' => 'mailto:' . trim((string) $p['PEC_condomino']),
'is_principal' => false,
];
}
if (! empty($p['Cell_cond']) && ! $this->hasContactValue($tableRows, (string) $p['Cell_cond'])) {
$tableRows[] = [
'canale' => 'Cellulare',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => 'Cellulare Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'valore' => trim((string) $p['Cell_cond']),
'action_url' => 'tel:' . trim((string) $p['Cell_cond']),
'is_principal' => false,
];
}
if (! empty($p['tel1']) && ! $this->hasContactValue($tableRows, (string) $p['tel1'])) {
$tableRows[] = [
'canale' => 'Telefono Fisso',
'icon' => '☎️',
'color' => 'bg-purple-50 text-purple-700 border-purple-200',
'etichetta' => 'Fisso Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'valore' => trim((string) $p['tel1']),
'action_url' => 'tel:' . trim((string) $p['tel1']),
'is_principal' => false,
];
}
if (! empty($p['E_mail_inquilino']) && ! $this->hasContactValue($tableRows, (string) $p['E_mail_inquilino'])) {
$tableRows[] = [
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => 'Email Inquilino',
'soggetto' => $inqName,
'ruolo' => 'Inquilino',
'valore' => trim((string) $p['E_mail_inquilino']),
'action_url' => 'mailto:' . trim((string) $p['E_mail_inquilino']),
'is_principal' => false,
];
}
if (! empty($p['Cell_inq']) && ! $this->hasContactValue($tableRows, (string) $p['Cell_inq'])) {
$tableRows[] = [
'canale' => 'Cellulare',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => 'Cellulare Inquilino',
'soggetto' => $inqName,
'ruolo' => 'Inquilino',
'valore' => trim((string) $p['Cell_inq']),
'action_url' => 'tel:' . trim((string) $p['Cell_inq']),
'is_principal' => false,
];
}
}
return $tableRows;
}
private function hasContactValue(array $rows, string $value): bool
{
$vTrim = trim(mb_strtolower($value));
foreach ($rows as $r) {
if (trim(mb_strtolower((string) ($r['valore'] ?? ''))) === $vTrim) {
return true;
}
}
return false;
}
public function getTimelineSubentriCompletaProperty(): array public function getTimelineSubentriCompletaProperty(): array
{ {
if (! $this->unita || ! $this->unita->stabile) { if (! $this->unita || ! $this->unita->stabile) {

View File

@ -215,178 +215,186 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
</div> </div>
</x-filament::section> </x-filament::section>
<div class="grid gap-4 lg:grid-cols-3"> {{-- 1. SCHEDE ANAGRAFICHE SOGGETTI ATTIVI & IDENTIFICAZIONE COMPATTA --}}
<x-filament::section class="lg:col-span-2"> <div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<x-slot name="heading">Indicatori principali</x-slot> {{-- Card Condomino / Proprietario Attivo --}}
<x-slot name="description">Superficie, millesimi, percentuale, valore</x-slot> <div class="rounded-xl border border-indigo-100 bg-white p-4 shadow-xs">
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-4"> <div class="flex items-center justify-between border-b border-indigo-50 pb-2 mb-3">
@foreach([ <div class="flex items-center gap-2">
['Superficie', ($unita->superficie_commerciale ?? 0) . ' m²'], <span class="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-indigo-100 text-sm font-bold text-indigo-800">🏢</span>
['Millesimi', $unita->millesimi_proprieta ?? $unita->millesimi_generali ?? 0], <span class="text-xs font-bold uppercase tracking-wider text-indigo-900">Condomino (Proprietario)</span>
['Percentuale', ($unita->millesimi_proprieta ?? $unita->millesimi_generali) ? number_format((($unita->millesimi_proprieta ?? $unita->millesimi_generali)/1000)*100,2) . '%' : '0%'],
['Valore stimato', ($unita->valore_commerciale_stimato ?? 0) . ' €'],
] as [$label, $value])
<x-filament::section class="p-4!">
<div class="text-xs text-gray-500">{{ $label }}</div>
<div class="text-lg font-semibold text-gray-900">{{ $value }}</div>
</x-filament::section>
@endforeach
</div> </div>
</x-filament::section> <span class="rounded-full bg-emerald-50 border border-emerald-200 px-2 py-0.5 text-[10px] font-bold text-emerald-700">🟢 In Carica</span>
<x-filament::section>
<x-slot name="heading">Identificazione</x-slot>
<x-slot name="description">Tipologia e stato</x-slot>
<div class="space-y-2">
@foreach([
['Tipologia', $unita->tipo_unita ?? 'ND'],
['Stato occupazione', $unita->stato_occupazione ?? 'ND'],
['Utilizzo', $unita->utilizzo_attuale ?? 'ND'],
['Codice stabile', $unita->stabile?->codice_operatore ?? $unita->stabile?->codice_stabile ?? 'ND'],
] as [$label, $value])
<x-filament::section class="p-3! bg-gray-50">
<div class="text-xs text-gray-500">{{ $label }}</div>
<div class="text-sm font-semibold text-gray-900">{{ $value }}</div>
</x-filament::section>
@endforeach
</div>
</x-filament::section>
</div> </div>
<div class="space-y-1.5">
<div class="text-base font-bold text-gray-900 leading-snug">
{{ $condominoNome !== '' ? $condominoNome : '—' }}
</div>
<div class="text-xs font-mono text-gray-600">
CF: <strong>{{ $condominoCf !== '' ? $condominoCf : '—' }}</strong>
</div>
<div class="flex items-center gap-2 text-xs text-gray-600 pt-1">
<span class="rounded bg-indigo-50 text-indigo-700 font-semibold px-2 py-0.5">
Quota: {{ $relazioniPerTipo['proprietari'][0]['quota_label'] ?? '100,00' }}%
</span>
@if($comproprietariCount > 0)
<span class="rounded bg-amber-50 text-amber-800 font-semibold px-2 py-0.5">
+{{ $comproprietariCount }} comproprietari
</span>
@endif
</div>
<div class="text-xs text-gray-500 pt-1">
Validità: <strong class="text-gray-700">{{ $relazioniPerTipo['proprietari'][0]['data_inizio'] ?? 'Gestione attiva' }}</strong>
</div>
</div>
</div>
{{-- Card Inquilino / Conduttore Attivo --}}
<x-filament::section> <div class="rounded-xl border border-sky-100 bg-white p-4 shadow-xs">
<x-slot name="heading">Persone collegate</x-slot> <div class="flex items-center justify-between border-b border-sky-50 pb-2 mb-3">
<x-slot name="description">Proprietari, inquilini, altri</x-slot> <div class="flex items-center gap-2">
<div class="grid gap-3 md:grid-cols-2 lg:grid-cols-3"> <span class="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-sky-100 text-sm font-bold text-sky-800">👤</span>
@foreach([ <span class="text-xs font-bold uppercase tracking-wider text-sky-900">Inquilino (Conduttore)</span>
'inquilini' => ['label' => 'Inquilini', 'color' => 'text-sky-700'], </div>
'altri' => ['label' => 'Altri soggetti', 'color' => 'text-gray-700'], @if($hasInquilino)
] as $key => $cfg) <span class="rounded-full bg-sky-50 border border-sky-200 px-2 py-0.5 text-[10px] font-bold text-sky-700">Locazione attiva</span>
@php $relazioni = $relazioniPerTipo[$key] ?? []; @endphp
<x-filament::section class="bg-gray-50">
<div class="mb-2 text-sm font-semibold {{ $cfg['color'] }}">{{ $cfg['label'] }} ({{ count($relazioni) }})</div>
@if(empty($relazioni))
<div class="text-sm text-gray-500">Nessun soggetto.</div>
@else @else
<div class="space-y-2"> <span class="rounded-full bg-gray-100 text-gray-500 px-2 py-0.5 text-[10px] font-medium">Nessun inquilino</span>
@foreach($relazioni as $relazione) @endif
</div>
<div class="space-y-1.5">
<div class="text-base font-bold text-gray-900 leading-snug">
{{ $inquilinoNome !== '' ? $inquilinoNome : 'Nessun inquilino registrato' }}
</div>
<div class="text-xs font-mono text-gray-600">
CF: <strong>{{ $relazioniPerTipo['inquilini'][0]['codice_fiscale'] ?? '—' }}</strong>
</div>
@if($hasInquilino)
<div class="flex items-center gap-2 text-xs text-gray-600 pt-1">
<span class="rounded bg-sky-50 text-sky-700 font-semibold px-2 py-0.5">
Spese: {{ $relazioniPerTipo['inquilini'][0]['quota_label'] ?? '100,00' }}%
</span>
</div>
<div class="text-xs text-gray-500 pt-1">
Subentro: <strong class="text-gray-700">{{ $relazioniPerTipo['inquilini'][0]['data_inizio'] ?? '—' }}</strong>
</div>
@else
<div class="text-xs text-gray-400 italic pt-2">
L'unità risulta occupata direttamente dal proprietario.
</div>
@endif
</div>
</div>
{{-- Card Dati Catastali & Identificazione Immobile --}}
<div class="rounded-xl border border-slate-200 bg-slate-50/70 p-4 shadow-xs">
<div class="flex items-center justify-between border-b border-slate-200 pb-2 mb-3">
<div class="flex items-center gap-2">
<span class="inline-flex h-7 w-7 items-center justify-center rounded-lg bg-slate-200 text-sm font-bold text-slate-800">🏛️</span>
<span class="text-xs font-bold uppercase tracking-wider text-slate-700">Dati Catasto & Immobile</span>
</div>
<span class="font-mono text-xs font-bold text-indigo-700">Sub. {{ $unita->subalterno ?: '—' }}</span>
</div>
<div class="space-y-1.5 text-xs">
<div class="flex items-center justify-between text-gray-600">
<span>Tipologia:</span>
<strong class="text-gray-900 capitalize">{{ $unita->tipo_unita ?? 'Abitazione' }}</strong>
</div>
<div class="flex items-center justify-between text-gray-600">
<span>Categoria & Rendita:</span>
<strong class="text-gray-900">{{ $unita->categoria_catastale ?: '—' }} · {{ number_format((float)($unita->rendita_catastale ?? 0), 2, ',', '.') }}</strong>
</div>
<div class="flex items-center justify-between text-gray-600">
<span>Foglio / Particella:</span>
<strong class="text-gray-900">Fg. {{ $unita->foglio ?: ($unita->stabile?->foglio ?: '—') }} / Part. {{ $unita->particella ?: ($unita->stabile?->particella_catasto ?: '—') }}</strong>
</div>
<div class="flex items-center justify-between text-gray-600">
<span>Posizione fabbricato:</span>
<strong class="text-gray-900">Scala {{ $unita->scala ?: '—' }} · Int. {{ $unita->interno ?: '—' }} · Piano {{ $unita->piano ?: '—' }}</strong>
</div>
</div>
</div>
</div>
{{-- 2. SEZIONE RECAPITI MULTICANALE DINAMICI (MATRICE TABELLARE COMPATTA) --}}
@php @php
$tipoRawRelazione = strtolower(trim((string) ($relazione['tipo_raw'] ?? ''))); $multiRecapiti = $this->getRecapitiMulticanaleTableProperty();
$roleBadge = null;
if ($key === 'proprietari') {
$roleBadge = $tipoRawRelazione === 'comproprietario'
? 'Comproprietario'
: 'Condomino di riferimento';
}
@endphp @endphp
<x-filament::section class="p-3!">
<div class="flex items-start justify-between gap-2"> <div class="rounded-xl border border-gray-200 bg-white shadow-xs overflow-hidden mt-4">
<div class="text-sm font-semibold text-gray-900">{{ $relazione['nome'] }}</div> <div class="flex items-center justify-between bg-gray-50/80 px-4 py-3 border-b border-gray-200">
@if($roleBadge) <div class="flex items-center gap-2">
<span class="rounded-full border border-gray-200 bg-white px-2 py-0.5 text-[11px] font-semibold text-gray-600">{{ $roleBadge }}</span> <span class="text-base">📱</span>
@endif <div>
<div class="text-sm font-bold text-gray-900">Recapiti Multicanale Dinamici dell'Unità</div>
<div class="text-[11px] text-gray-500">Email, PEC, cellulari, telefoni e recapiti postali dei soggetti collegati</div>
</div> </div>
<div class="text-xs text-gray-500">CF: {{ $relazione['codice_fiscale'] ?? '—' }}</div>
<div class="text-xs text-gray-500">Quota: {{ $relazione['quota_label'] ?? '—' }}%</div>
@php
$dal = $relazione['data_inizio'] ?? null;
$al = $relazione['data_fine'] ?? null;
@endphp
<div class="text-xs text-gray-500">
Periodo:
@if($dal || $al)
@if($dal) Da {{ $dal }} @endif
@if($al) · fino a {{ $al }} @endif
@else
Da sempre
@endif
</div> </div>
@if(!empty($relazione['ruolo_rate'])) <span class="rounded-full bg-indigo-50 border border-indigo-200 px-2.5 py-0.5 text-xs font-bold text-indigo-700">
<div class="text-xs text-gray-500">Ruolo rate: ({{ $relazione['ruolo_rate'] }})</div> {{ count($multiRecapiti) }} canali attivi
@endif </span>
</x-filament::section>
@endforeach
</div>
@endif
</x-filament::section>
@endforeach
</div> </div>
<div class="mt-6"> @if(empty($multiRecapiti))
<div class="text-sm font-semibold text-gray-900 mb-2">Recapiti email per servizio</div> <div class="p-6 text-center text-xs text-gray-500">
@if(empty($recapitiServizio)) Nessun recapito multicanale o contatto registrato per i soggetti di questa unità.
<div class="text-sm text-gray-500">Nessun recapito email disponibile.</div> </div>
@else @else
<div class="grid gap-3 lg:grid-cols-2"> <div class="overflow-x-auto">
@foreach($recapitiServizio as $service) <table class="w-full text-left text-xs text-gray-700 divide-y divide-gray-100">
<div class="rounded-xl border bg-gray-50 p-3"> <thead class="bg-gray-50 text-[11px] font-bold uppercase tracking-wider text-gray-500">
<div class="text-sm font-semibold text-gray-900">{{ $service['label'] ?? 'Servizio' }}</div> <tr>
@if(empty($service['rows'])) <th class="px-4 py-2.5">Canale</th>
<div class="mt-2 text-sm text-gray-500">Nessun recapito risolto.</div> <th class="px-4 py-2.5">Etichetta / Uso</th>
<th class="px-4 py-2.5">Soggetto Collegato</th>
<th class="px-4 py-2.5">Valore Contatto</th>
<th class="px-4 py-2.5 text-center">Stato</th>
<th class="px-4 py-2.5 text-right">Azioni Rapide</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
@foreach($multiRecapiti as $rec)
<tr class="hover:bg-indigo-50/30 transition">
<td class="px-4 py-2.5 whitespace-nowrap">
<span class="inline-flex items-center gap-1.5 rounded-md border px-2 py-0.5 font-semibold text-[11px] {{ $rec['color'] }}">
<span>{{ $rec['icon'] }}</span>
<span>{{ $rec['canale'] }}</span>
</span>
</td>
<td class="px-4 py-2.5 font-medium text-gray-800 whitespace-nowrap">
{{ $rec['etichetta'] }}
</td>
<td class="px-4 py-2.5 whitespace-nowrap">
<span class="font-bold text-gray-900">{{ $rec['soggetto'] }}</span>
<span class="text-gray-400 font-normal">({{ $rec['ruolo'] }})</span>
</td>
<td class="px-4 py-2.5 font-mono text-gray-900 font-medium">
{{ $rec['valore'] }}
</td>
<td class="px-4 py-2.5 text-center whitespace-nowrap">
@if($rec['is_principal'])
<span class="rounded bg-emerald-100 text-emerald-800 px-1.5 py-0.5 text-[10px] font-bold"> Principale</span>
@else @else
<div class="mt-2 space-y-2"> <span class="text-gray-400 text-[11px]"></span>
@foreach($service['rows'] as $recipient) @endif
<div class="rounded-lg border bg-white p-3"> </td>
<div class="text-sm font-semibold text-gray-900">{{ $recipient['email'] ?? '—' }}</div> <td class="px-4 py-2.5 text-right whitespace-nowrap">
<div class="text-xs text-gray-500">{{ $recipient['persona'] ?: 'Recapito unità' }} · {{ $recipient['sourceLabel'] ?? 'Anagrafica persona' }}</div> @if(!empty($rec['action_url']))
</div> <a href="{{ $rec['action_url'] }}" class="inline-flex items-center gap-1 rounded bg-indigo-50 hover:bg-indigo-100 border border-indigo-200 text-indigo-700 font-semibold px-2 py-1 text-[11px] transition">
<span>Apri</span>
</a>
@endif
</td>
</tr>
@endforeach @endforeach
</tbody>
</table>
</div> </div>
@endif @endif
</div> </div>
@endforeach
</div>
@endif
</div>
<div class="mt-6 border-t pt-4">
<div class="flex items-center gap-2 mb-3">
<x-filament::icon icon="heroicon-o-clock" class="h-5 w-5 text-amber-600" />
<span class="text-base font-bold text-gray-900">Timeline Subentri e Gestioni Anteriore</span>
</div>
<div class="text-xs text-gray-500 mb-3">Cronistoria titolari ed avvicendamenti storici per questa unità immobiliare tra le diverse gestioni</div>
@if(empty($nominativiStorici))
<div class="rounded-lg border border-dashed border-gray-200 p-4 text-center text-sm text-gray-500">Nessun avvicendamento o storico subentri registrato per questa unità.</div>
@else
<div class="space-y-3">
@foreach($nominativiStorici as $item)
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-xs transition hover:border-amber-300">
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-wrap items-center gap-2">
<span class="rounded-md bg-amber-50 border border-amber-200 px-2.5 py-1 text-xs font-bold text-amber-800">
{{ $item['periodo'] ?? 'Da sempre' }}
</span>
<span class="text-base font-bold text-gray-900">
{{ $item['nominativo'] ?? $item['nome_completo'] ?? $item['nome'] ?? '—' }}
</span>
@if(!empty($item['ruolo']))
<span class="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-semibold text-gray-700">
{{ $item['ruolo'] }}
</span>
@endif
@if(!empty($item['percentuale']))
<span class="rounded-full bg-blue-50 text-blue-700 border border-blue-200 px-2.5 py-0.5 text-xs font-semibold">
Quota: {{ $item['percentuale'] }}
</span>
@endif
</div>
@if(!empty($item['fonte']))
<div class="text-xs text-gray-500 font-medium">Fonte: {{ $item['fonte'] }}</div>
@endif
</div>
@if(!empty($item['detail']))
<div class="mt-2 text-xs font-medium text-gray-600 bg-gray-50 rounded-lg p-2.5">
{{ $item['detail'] }}
</div>
@endif
</div>
@endforeach
</div>
@endif
</div>
</x-filament::section>
@endif @endif
@if($tab === 'nominativi') @if($tab === 'nominativi')
@ -430,8 +438,15 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
<span>Gestioni: <strong class="text-indigo-700">{{ implode(' | ', $item['year_labels'] ?? $item['years']) }}</strong></span> <span>Gestioni: <strong class="text-indigo-700">{{ implode(' | ', $item['year_labels'] ?? $item['years']) }}</strong></span>
</div> </div>
@if(!empty($item['dettaglio_subentro'])) @if(!empty($item['dettaglio_subentro']))
<div class="text-xs text-amber-700 bg-amber-50 rounded border border-amber-200 px-2 py-1 inline-block mt-1"> @php
{{ $item['dettaglio_subentro'] }} $subentroText = $item['dettaglio_subentro'];
try {
$parsedDate = \Carbon\Carbon::parse($subentroText)->format('d/m/Y');
$subentroText = 'Subentrato il: ' . $parsedDate;
} catch (\Throwable $e) {}
@endphp
<div class="text-xs text-amber-800 bg-amber-50 rounded-md border border-amber-200 px-2.5 py-1 inline-block mt-1 font-medium">
📅 {{ $subentroText }}
</div> </div>
@endif @endif
</div> </div>
@ -439,9 +454,9 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
<div> <div>
<button type="button" <button type="button"
wire:click="selectNominativoKey('{{ $item['key'] }}')" wire:click="selectNominativoKey('{{ $item['key'] }}')"
class="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3 py-2 text-xs font-semibold text-white shadow-xs hover:bg-indigo-700 transition"> class="inline-flex items-center gap-1.5 rounded-lg bg-indigo-600 px-3.5 py-2 text-xs font-semibold text-white shadow-xs hover:bg-indigo-700 transition">
<span>🧾</span> <span>🧾</span>
<span>Vedi Estratto Conto per Nominativo</span> <span>Vai all'Estratto Conto del Soggetto</span>
</button> </button>
</div> </div>
</div> </div>

View File

@ -11,20 +11,15 @@ ## Obiettivo
- Allineamento delle finestre temporali (`data_inizio`, `data_fine`) in `unita_anagrafica_periodo` per tutti i subentri storici dello Stabile 0013. - Allineamento delle finestre temporali (`data_inizio`, `data_fine`) in `unita_anagrafica_periodo` per tutti i subentri storici dello Stabile 0013.
- Risoluzione dinamica dei nominativi nel selettore a tendina e nella scheda unità in base all'`anno_gestione` attivo (2026, 2024, 2020, ecc.). - Risoluzione dinamica dei nominativi nel selettore a tendina e nella scheda unità in base all'`anno_gestione` attivo (2026, 2024, 2020, ecc.).
2. **Allineamento Catasto e Subentri Esemplari**: 2. **Allineamento Catasto e Subentri Esemplari**:
- **Scala B Int. 6 (ID 247)**: - **Scala B Int. 6 (ID 247)**: COCCIA Simone (2026) vs DE SANTIS Fabio (2020) con allineamento Sister Catasto (Sub. 25, Rendita € 742,41).
- Gestione 2026: visualizza l'attuale proprietario **COCCIA Simone** (CF: `CCCSMN74B06L120H`), allineato a Sister Catasto (**Fg. 403, Part. 60, Sub. 25, Cat. A/2, Cl. 03, Vani 2.5, Rendita € 742,41**, Proprietà 1/1). - **Scala B Int. 1 (ID 242)**: Tabacchini Giuseppe (2026) vs PAVIA Monica (2024).
- Gestioni fino al 2021 (es. 2020): visualizza il dante causa **DE SANTIS Fabio** (fino al 28/09/2021). - **Scala B Int. 3 / B Int. 4**: RDP S.R.L. limitata esclusivamente a queste due unità dal 08/04/2026, rimossa da tutte le altre unità di Scala B.
- **Scala B Int. 1 (ID 242)**: - **Scala A Int. 1 (ID 233)**: Spadavecchia Aldo (condomino) e Kiwa Cermet Spa (inquilino), bonificati i comproprietari incrociati orfani.
- Gestione 2026: visualizza **Tabacchini Giuseppe** (subentrato dal 30/10/2025). 3. **Restyling Compatto Tab Riepilogo & Matrice Recapiti Multicanale**:
- Gestioni fino al 2025 (es. 2024): visualizza **PAVIA Monica** (con storico inquilino). - Eliminazione dei grossi box "Indicatori principali" (Superficie, millesimi, valore) e dei 7 riquadri vuoti dei servizi.
- **Scala B Int. 3 / B Int. 4**: - Inserimento schede compatte anagrafiche: Condomino (Proprietario) attivo con quota e CF, Inquilino attivo con quota spese, Dati Catasto e Fabbricato.
- Fino al 07/04/2026: **PICCOLO Pasqualina EREDI**. - Nuova tabella ad alta densità informativa **Recapiti Multicanale Dinamici** (Email, PEC, Cellulari, Telefoni Fissi, Indirizzo) con badge colorati, stato principale e pulsanti rapidi di apertura `mailto:` e `tel:`.
- Dal 08/04/2026: **RDP S.R.L.**. - Rimozione della vecchia timeline ridondante dal Riepilogo; il tab **Nominativi & Subentri** ora gestisce la cronistoria temporale completa con date formattate e pulsante diretto `[ 🧾 Vai all'Estratto Conto del Soggetto ]`.
3. **Bonifica e Allineamento Stabile 0013**:
- Comando Artisan idempotente `php artisan gescon:bonifica-stabile-0013` esteso con riallineamento temporale automatico di tutti i subentri e dei dati catastali ufficiali AdE.
4. **Modernizzazione Blade Scheda Stabile e Compattazione Dropdown**:
- Nuova barra di navigazione unificata, Hero Header Banner dello Stabile con badge e quick actions, tab navigation pill-bar.
- Compattazione dei comproprietari multipli nel dropdown (`Primary (+N)`).
## Output del Giro Operativo ## Output del Giro Operativo
@ -32,23 +27,20 @@ ## Output del Giro Operativo
TASK_ID: task-87b64082c1 TASK_ID: task-87b64082c1
REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git REPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git
BRANCH: stabilization/205-zero BRANCH: stabilization/205-zero
COMMIT: cb6249b COMMIT: in_pubblicazione
FILE_O_AREE_TOCCATE: FILE_O_AREE_TOCCATE:
- app/Console/Commands/GesconBonificaStabile0013Command.php - app/Console/Commands/GesconBonificaStabile0013Command.php
- app/Models/UnitaImmobiliare.php - app/Models/UnitaImmobiliare.php
- app/Filament/Pages/UnitaImmobiliarePage.php - app/Filament/Pages/UnitaImmobiliarePage.php
- app/Filament/Pages/Condomini/CatastoHub.php
- resources/views/filament/pages/unita-immobiliare.blade.php - resources/views/filament/pages/unita-immobiliare.blade.php
- resources/views/filament/pages/condomini/stabile.blade.php
- tests/Feature/UnitaGestioneTemporaleTest.php - tests/Feature/UnitaGestioneTemporaleTest.php
- tests/Feature/CatastoHubDbDrivenTest.php - tests/Feature/CatastoHubDbDrivenTest.php
TEST_ESEGUITI: TEST_ESEGUITI:
- ./vendor/bin/pest 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 (20 passed, 115 assertions) - ./vendor/bin/pest 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 (20 passed, 115 assertions)
GATE_STATISTICS: GATE_STATISTICS:
- UNITA_REALI_0013: 42 unità canoniche con cronistoria temporale multi-gestione. - UNITA_REALI_0013: 42 unità canoniche con cronistoria temporale multi-gestione.
- TEMPORAL_DYNAMIC_OWNER: Risoluzione dinamica proprietario per anno di gestione su B-6, B-1, C-2, C-5, B-8, NGOT-103. - RDP_CLEANUP: RDP S.R.L. circoscritta esclusivamente a B-3 e B-4, rimossa da B-1, B-2, B-5, B-7.
- CATASTO_SISTER_B6: Subalterno 25, Rendita 742,41, Intestato COCCIA Simone 1/1. - MULTICHANNEL_CONTACTS_TABLE: Matrice recapiti multicanale tabellare compatta e responsive.
- DROPDOWN_COMPACT: Nominativi multipli compattati a singola riga `Nome (+N)`.
- ZERO_MOCK: Dati catastali 100% DB-driven e Sister API live. - ZERO_MOCK: Dati catastali 100% DB-driven e Sister API live.
BLOCCO_DATI: no BLOCCO_DATI: no
BLOCCO_CONTRATTO: no BLOCCO_CONTRATTO: no
@ -57,5 +49,5 @@ ## Output del Giro Operativo
## Prossimo Passo per .200 (Validazione) ## Prossimo Passo per .200 (Validazione)
- Eseguire il checkout del branch `stabilization/205-zero`. - Eseguire il checkout del branch `stabilization/205-zero`.
- Eseguire `php artisan gescon:bonifica-stabile-0013` per convalidare la bonifica e l'allineamento temporale. - Eseguire `php artisan gescon:bonifica-stabile-0013` per convalidare la bonifica, l'eliminazione dei comproprietari incrociati e l'allineamento temporale.
- Eseguire la suite di test Pest (20 test, 115 asserzioni). - Eseguire la suite di test Pest (20 test, 115 asserzioni).