feat(contabilita): atomic multicanale contacts, CF conguaglio badges and modernized situazione iniziale UI

This commit is contained in:
michele 2026-09-03 16:27:01 +02:00
parent 88829fcace
commit 3699d388cd
5 changed files with 1036 additions and 868 deletions

View File

@ -1281,10 +1281,7 @@ private function loadDettTabCrudRows(string $codStabileLegacy, ?string $legacyYe
}
$q = DB::connection('gescon_import')->table('dett_tab')
->where('cod_tab', 'CONG.O')
->where(function ($query) {
$query->whereNull('n_stra')->orWhere('n_stra', '<=', 0);
});
->where('cod_tab', 'like', 'CONG.%');
if (Schema::connection('gescon_import')->hasColumn('dett_tab', 'cod_stabile')) {
$hasForStabile = DB::connection('gescon_import')->table('dett_tab')
@ -1458,15 +1455,17 @@ public function saveDettTabRow(int $id): void
$cons = 0.0;
}
$codTab = ! empty($edit['cod_tab']) ? trim((string) $edit['cod_tab']) : 'CONG.O';
DB::connection('gescon_import')->table('dett_tab')
->where('id', $id)
->update([
'cod_tab' => 'CONG.O',
'cod_tab' => $codTab,
'id_cond' => is_string($edit['id_cond'] ?? null) ? trim((string) $edit['id_cond']) : null,
'cond_inquil' => is_string($edit['cond_inquil'] ?? null) ? trim((string) $edit['cond_inquil']) : null,
'mm' => is_numeric($edit['mm'] ?? null) ? (float) $edit['mm'] : null,
'cons_euro' => round((float) $cons, 2),
'n_stra' => null,
'n_stra' => is_numeric($edit['n_stra'] ?? null) ? (int) $edit['n_stra'] : null,
'unico' => ! empty($edit['unico']) ? 1 : 0,
'updated_at' => now(),
]);
@ -1497,7 +1496,7 @@ public function addDettTabRow(): void
return;
}
$codTab = 'CONG.O';
$codTab = ! empty($this->formDettTabAdd['cod_tab']) ? trim((string) $this->formDettTabAdd['cod_tab']) : 'CONG.O';
$idCond = is_string($this->formDettTabAdd['id_cond'] ?? null) ? trim((string) $this->formDettTabAdd['id_cond']) : '';
if ($codTab === '' || $idCond === '') {
return;
@ -1514,7 +1513,7 @@ public function addDettTabRow(): void
'cond_inquil' => is_string($this->formDettTabAdd['cond_inquil'] ?? null) ? trim((string) $this->formDettTabAdd['cond_inquil']) : null,
'mm' => is_numeric($this->formDettTabAdd['mm'] ?? null) ? (float) $this->formDettTabAdd['mm'] : null,
'cons_euro' => round((float) $cons, 2),
'n_stra' => null,
'n_stra' => is_numeric($this->formDettTabAdd['n_stra'] ?? null) ? (int) $this->formDettTabAdd['n_stra'] : null,
'unico' => ! empty($this->formDettTabAdd['unico']) ? 1 : 0,
'created_at' => now(),
'updated_at' => now(),

View File

@ -159,6 +159,8 @@ public static function canAccess(): bool
*/
public string $estrattoTipo = 'condomini';
public string $recapitiViewMode = 'diviso';
/**
* @var array<int, string>
*/
@ -539,6 +541,8 @@ public function getRecapitiMulticanaleTableProperty(): array
continue;
}
$tipoSoggetto = in_array(strtolower($ruolo), ['inquilino', 'conduttore', 'locatario'], true) || (string) ($entity['tipo_relazione'] ?? '') === 'inquilino' ? 'inquilino' : 'condomino';
$anagId = (int) ($entity['persona_id'] ?? ($entity['soggetto_id'] ?? 0));
$anag = null;
if ($anagId > 0) {
@ -589,15 +593,16 @@ public function getRecapitiMulticanaleTableProperty(): array
$cleanPrefix = ucwords(str_replace(['.', '_', '-'], ' ', $prefix));
$lbl = count($parts) > 1 ? ($idx === 0 ? 'Principale (' . $cleanPrefix . ')' : 'Email ' . $cleanPrefix) : 'Principale';
$tableRows[] = [
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => $lbl,
'soggetto' => $nome,
'ruolo' => $ruolo,
'valore' => $em,
'action_url' => 'mailto:' . $em,
'is_principal' => $idx === 0 && count($tableRows) === 0,
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => $lbl,
'soggetto' => $nome,
'ruolo' => $ruolo,
'tipo_soggetto' => $tipoSoggetto,
'valore' => $em,
'action_url' => 'mailto:' . $em,
'is_principal' => $idx === 0 && count($tableRows) === 0,
];
}
}
@ -608,15 +613,16 @@ public function getRecapitiMulticanaleTableProperty(): array
$pc = trim($pc);
if ($pc !== '' && ! $this->hasContactValue($tableRows, $pc)) {
$tableRows[] = [
'canale' => 'Email PEC',
'icon' => '🛡️',
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
'etichetta' => count($parts) > 1 ? 'PEC #' . ($idx + 1) : 'PEC Ufficiale',
'soggetto' => $nome,
'ruolo' => $ruolo,
'valore' => $pc,
'action_url' => 'mailto:' . $pc,
'is_principal' => false,
'canale' => 'Email PEC',
'icon' => '🛡️',
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
'etichetta' => count($parts) > 1 ? 'PEC #' . ($idx + 1) : 'PEC Ufficiale',
'soggetto' => $nome,
'ruolo' => $ruolo,
'tipo_soggetto' => $tipoSoggetto,
'valore' => $pc,
'action_url' => 'mailto:' . $pc,
'is_principal' => false,
];
}
}
@ -627,30 +633,32 @@ public function getRecapitiMulticanaleTableProperty(): array
$ph = trim($ph);
if ($ph !== '' && ! $this->hasContactValue($tableRows, $ph)) {
$tableRows[] = [
'canale' => 'Cellulare / Tel',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => count($parts) > 1 ? 'Telefono #' . ($idx + 1) : 'Personale',
'soggetto' => $nome,
'ruolo' => $ruolo,
'valore' => $ph,
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
'is_principal' => false,
'canale' => 'Cellulare / Tel',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => count($parts) > 1 ? 'Telefono #' . ($idx + 1) : 'Personale',
'soggetto' => $nome,
'ruolo' => $ruolo,
'tipo_soggetto' => $tipoSoggetto,
'valore' => $ph,
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
'is_principal' => false,
];
}
}
}
if ($rawInd !== '') {
$tableRows[] = [
'canale' => 'Indirizzo',
'icon' => '📍',
'color' => 'bg-slate-50 text-slate-700 border-slate-200',
'etichetta' => 'Residenza / Sede',
'soggetto' => $nome,
'ruolo' => $ruolo,
'valore' => $rawInd,
'action_url' => null,
'is_principal' => false,
'canale' => 'Indirizzo',
'icon' => '📍',
'color' => 'bg-slate-50 text-slate-700 border-slate-200',
'etichetta' => 'Residenza / Sede',
'soggetto' => $nome,
'ruolo' => $ruolo,
'tipo_soggetto' => $tipoSoggetto,
'valore' => $rawInd,
'action_url' => null,
'is_principal' => false,
];
}
}
@ -671,15 +679,16 @@ public function getRecapitiMulticanaleTableProperty(): array
$cleanPrefix = ucwords(str_replace(['.', '_', '-'], ' ', $prefix));
$lbl = count($parts) > 1 ? ($idx === 0 ? 'Email Condomino (' . $cleanPrefix . ')' : 'Email ' . $cleanPrefix) : 'Email Condomino';
$tableRows[] = [
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => $lbl,
'soggetto' => $condName,
'ruolo' => 'Condomino',
'valore' => $em,
'action_url' => 'mailto:' . $em,
'is_principal' => count($tableRows) === 0,
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => $lbl,
'soggetto' => $condName,
'ruolo' => 'Condomino',
'tipo_soggetto' => 'condomino',
'valore' => $em,
'action_url' => 'mailto:' . $em,
'is_principal' => count($tableRows) === 0,
];
}
}
@ -690,15 +699,16 @@ public function getRecapitiMulticanaleTableProperty(): array
$pc = trim($pc);
if ($pc !== '' && ! $this->hasContactValue($tableRows, $pc)) {
$tableRows[] = [
'canale' => 'Email PEC',
'icon' => '🛡️',
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
'etichetta' => count($parts) > 1 ? 'PEC Condomino #' . ($idx + 1) : 'PEC Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'valore' => $pc,
'action_url' => 'mailto:' . $pc,
'is_principal' => false,
'canale' => 'Email PEC',
'icon' => '🛡️',
'color' => 'bg-emerald-50 text-emerald-700 border-emerald-200',
'etichetta' => count($parts) > 1 ? 'PEC Condomino #' . ($idx + 1) : 'PEC Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'tipo_soggetto' => 'condomino',
'valore' => $pc,
'action_url' => 'mailto:' . $pc,
'is_principal' => false,
];
}
}
@ -709,15 +719,16 @@ public function getRecapitiMulticanaleTableProperty(): array
$ph = trim($ph);
if ($ph !== '' && ! $this->hasContactValue($tableRows, $ph)) {
$tableRows[] = [
'canale' => 'Cellulare',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => count($parts) > 1 ? 'Cellulare Condomino #' . ($idx + 1) : 'Cellulare Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'valore' => $ph,
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
'is_principal' => false,
'canale' => 'Cellulare',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => count($parts) > 1 ? 'Cellulare Condomino #' . ($idx + 1) : 'Cellulare Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'tipo_soggetto' => 'condomino',
'valore' => $ph,
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
'is_principal' => false,
];
}
}
@ -728,15 +739,16 @@ public function getRecapitiMulticanaleTableProperty(): array
$ph = trim($ph);
if ($ph !== '' && ! $this->hasContactValue($tableRows, $ph)) {
$tableRows[] = [
'canale' => 'Telefono Fisso',
'icon' => '☎️',
'color' => 'bg-purple-50 text-purple-700 border-purple-200',
'etichetta' => count($parts) > 1 ? 'Fisso Condomino #' . ($idx + 1) : 'Fisso Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'valore' => $ph,
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
'is_principal' => false,
'canale' => 'Telefono Fisso',
'icon' => '☎️',
'color' => 'bg-purple-50 text-purple-700 border-purple-200',
'etichetta' => count($parts) > 1 ? 'Fisso Condomino #' . ($idx + 1) : 'Fisso Condomino',
'soggetto' => $condName,
'ruolo' => 'Condomino',
'tipo_soggetto' => 'condomino',
'valore' => $ph,
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
'is_principal' => false,
];
}
}
@ -750,15 +762,16 @@ public function getRecapitiMulticanaleTableProperty(): array
$cleanPrefix = ucwords(str_replace(['.', '_', '-'], ' ', $prefix));
$lbl = count($parts) > 1 ? ($idx === 0 ? 'Email Inquilino (' . $cleanPrefix . ')' : 'Email ' . $cleanPrefix) : 'Email Inquilino';
$tableRows[] = [
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => $lbl,
'soggetto' => $inqName,
'ruolo' => 'Inquilino',
'valore' => $em,
'action_url' => 'mailto:' . $em,
'is_principal' => false,
'canale' => 'Email',
'icon' => '✉️',
'color' => 'bg-blue-50 text-blue-700 border-blue-200',
'etichetta' => $lbl,
'soggetto' => $inqName,
'ruolo' => 'Inquilino',
'tipo_soggetto' => 'inquilino',
'valore' => $em,
'action_url' => 'mailto:' . $em,
'is_principal' => false,
];
}
}
@ -769,15 +782,16 @@ public function getRecapitiMulticanaleTableProperty(): array
$ph = trim($ph);
if ($ph !== '' && ! $this->hasContactValue($tableRows, $ph)) {
$tableRows[] = [
'canale' => 'Cellulare',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => count($parts) > 1 ? 'Cellulare Inquilino #' . ($idx + 1) : 'Cellulare Inquilino',
'soggetto' => $inqName,
'ruolo' => 'Inquilino',
'valore' => $ph,
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
'is_principal' => false,
'canale' => 'Cellulare',
'icon' => '📱',
'color' => 'bg-amber-50 text-amber-700 border-amber-200',
'etichetta' => count($parts) > 1 ? 'Cellulare Inquilino #' . ($idx + 1) : 'Cellulare Inquilino',
'soggetto' => $inqName,
'ruolo' => 'Inquilino',
'tipo_soggetto' => 'inquilino',
'valore' => $ph,
'action_url' => 'tel:' . preg_replace('/[^\d\+]/', '', $ph),
'is_principal' => false,
];
}
}
@ -1563,6 +1577,14 @@ public function setEstrattoTipo(string $tipo): void
$this->condInquilActive = $tipo === 'inquilini' ? 'I' : 'C';
}
public function setRecapitiViewMode(string $mode): void
{
$mode = strtolower(trim($mode));
if (in_array($mode, ['diviso', 'tutti', 'condomino', 'inquilino'], true)) {
$this->recapitiViewMode = $mode;
}
}
/**
* @return array{0:string,1:int,2:string}
*/
@ -3813,11 +3835,67 @@ protected function hydrateRelazioni(): void
];
});
$uapRelations = collect();
if (DbSchema::hasTable('unita_anagrafica_periodo') && DbSchema::hasTable('anagrafiche')) {
try {
$startOfYear = "{$activeAnno}-01-01";
$endOfYear = "{$activeAnno}-12-31";
$uapList = DB::table('unita_anagrafica_periodo as uap')
->join('anagrafiche as a', 'uap.anagrafica_id', '=', 'a.id')
->where('uap.unita_immobiliare_id', $this->unita->id)
->where(function ($q) use ($endOfYear) {
$q->whereNull('uap.data_inizio')
->orWhere('uap.data_inizio', '<=', $endOfYear);
})
->where(function ($q) use ($startOfYear) {
$q->whereNull('uap.data_fine')
->orWhere('uap.data_fine', '>=', $startOfYear);
})
->orderBy('uap.id')
->select('uap.*', 'a.nome', 'a.cognome', 'a.ragione_sociale', 'a.codice_fiscale')
->get();
foreach ($uapList as $uRow) {
$nome = trim((string) ($uRow->ragione_sociale ?: ($uRow->cognome . ' ' . $uRow->nome)));
if ($nome === '') {
$nome = trim((string) ($uRow->nome . ' ' . $uRow->cognome));
}
$tipoRaw = strtolower(trim((string) ($uRow->ruolo_occupazione ?? 'condomino')));
$uapRelations->push([
'id' => (int) $uRow->id,
'persona_id' => (int) $uRow->anagrafica_id,
'legacy_persona_id' => (int) $uRow->anagrafica_id,
'rubrica_id' => null,
'nome' => $nome,
'codice_fiscale' => $uRow->codice_fiscale,
'tipo_raw' => $tipoRaw,
'tipo' => ucfirst(str_replace('_', ' ', $tipoRaw)),
'ruolo_custom' => null,
'has_custom_role' => false,
'ruolo_rate' => $tipoRaw === 'inquilino' ? 'I' : 'C',
'quota' => 100.0,
'quota_label' => '100,00',
'data_inizio' => $formatDate($uRow->data_inizio),
'data_fine' => $formatDate($uRow->data_fine),
'attivo' => $uRow->data_fine === null || $uRow->data_fine >= date('Y-m-d'),
'telefono' => '',
'email' => '',
'rubrica_url' => null,
'riceve_comunicazioni' => true,
'riceve_convocazioni' => true,
'vota_assemblea' => true,
]);
}
} catch (\Throwable $e) {}
}
$fallbackKeys = $fallbackRelations
->map(fn(array $rel) => strtolower(trim((string) ($rel['tipo_raw'] ?? ''))) . '|' . $this->relationIdentityKey($rel))
->all();
$relazioniMapped = $fallbackRelations
$relazioniMapped = $uapRelations
->concat($fallbackRelations)
->concat($relazioniMapped->reject(function (array $rel) use ($fallbackKeys): bool {
$key = strtolower(trim((string) ($rel['tipo_raw'] ?? ''))) . '|' . $this->relationIdentityKey($rel);
@ -4050,6 +4128,22 @@ protected function hydrateRelazioni(): void
if ($matchedInquilini->isNotEmpty()) {
$inquilini = $matchedInquilini;
$relazioniStoriche = array_values(array_unique(array_merge($relazioniStoriche, $unmatchedInquilini), SORT_REGULAR));
} else {
$legacyInqRecord = [
'persona_id' => null,
'nome' => $legacyInqName,
'codice_fiscale'=> trim((string) ($legacyRow?->inquil_cod_fisc ?? '')),
'tipo_relazione'=> 'inquilino',
'ruolo_label' => 'Inquilino / Conduttore',
'quota' => 100.0,
'quota_label' => '100,00',
'data_inizio' => 'Gestione ' . $activeAnno,
'data_fine' => null,
'attivo' => true,
'fonte' => 'legacy_condomin_mirror',
];
$relazioniStoriche = array_values(array_unique(array_merge($relazioniStoriche, $inquilini->all()), SORT_REGULAR));
$inquilini = collect([$legacyInqRecord]);
}
}

View File

@ -317,23 +317,47 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
</div>
</div>
{{-- 2. SEZIONE RECAPITI MULTICANALE DINAMICI (MATRICE TABELLARE COMPATTA) --}}
{{-- 2. SEZIONE RECAPITI MULTICANALE DINAMICI (MATRICE ATOMICA DIVISA COND/INQ + AGGREGATA) --}}
@php
$multiRecapiti = $this->getRecapitiMulticanaleTableProperty();
$recapitiCondomino = array_values(array_filter($multiRecapiti, fn($r) => ($r['tipo_soggetto'] ?? 'condomino') === 'condomino'));
$recapitiInquilino = array_values(array_filter($multiRecapiti, fn($r) => ($r['tipo_soggetto'] ?? '') === 'inquilino'));
$currentRecapitiMode = $this->recapitiViewMode ?? 'diviso';
@endphp
<div class="rounded-xl border border-gray-200 bg-white shadow-xs overflow-hidden mt-4">
<div class="flex items-center justify-between bg-gray-50/80 px-4 py-3 border-b border-gray-200">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between bg-gray-50/80 px-4 py-3 border-b border-gray-200 gap-3">
<div class="flex items-center gap-2">
<span class="text-base">📱</span>
<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 class="text-[11px] text-gray-500">Email, PEC, cellulari, telefoni e recapiti postali divisi per Condomino e Inquilino</div>
</div>
</div>
<span class="rounded-full bg-indigo-50 border border-indigo-200 px-2.5 py-0.5 text-xs font-bold text-indigo-700">
{{ count($multiRecapiti) }} canali attivi
</span>
{{-- Selettore Vista Atomica / Aggregata --}}
<div class="flex items-center gap-1.5 bg-white border border-gray-200 p-1 rounded-lg">
<button type="button"
wire:click="setRecapitiViewMode('diviso')"
class="px-2.5 py-1 text-xs font-semibold rounded-md transition {{ $currentRecapitiMode === 'diviso' ? 'bg-indigo-600 text-white shadow-xs' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100' }}">
📊 Diviso per Soggetto
</button>
<button type="button"
wire:click="setRecapitiViewMode('condomino')"
class="px-2.5 py-1 text-xs font-semibold rounded-md transition {{ $currentRecapitiMode === 'condomino' ? 'bg-amber-500 text-white shadow-xs' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100' }}">
🏢 Condomino ({{ count($recapitiCondomino) }})
</button>
<button type="button"
wire:click="setRecapitiViewMode('inquilino')"
class="px-2.5 py-1 text-xs font-semibold rounded-md transition {{ $currentRecapitiMode === 'inquilino' ? 'bg-indigo-500 text-white shadow-xs' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100' }}">
👤 Inquilino ({{ count($recapitiInquilino) }})
</button>
<button type="button"
wire:click="setRecapitiViewMode('tutti')"
class="px-2.5 py-1 text-xs font-semibold rounded-md transition {{ $currentRecapitiMode === 'tutti' ? 'bg-slate-700 text-white shadow-xs' : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100' }}">
🌐 Tutti ({{ count($multiRecapiti) }})
</button>
</div>
</div>
@if(empty($multiRecapiti))
@ -341,56 +365,195 @@ class="inline-flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-s
Nessun recapito multicanale o contatto registrato per i soggetti di questa unità.
</div>
@else
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-gray-700 divide-y divide-gray-100">
<thead class="bg-gray-50 text-[11px] font-bold uppercase tracking-wider text-gray-500">
<tr>
<th class="px-4 py-2.5">Canale</th>
<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
<span class="text-gray-400 text-[11px]"></span>
@endif
</td>
<td class="px-4 py-2.5 text-right whitespace-nowrap">
@if(!empty($rec['action_url']))
<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>
{{-- VISTA 1: DIVISO PER SOGGETTO (DUE SOTTO-TABELLE ATOMICHE) --}}
@if($currentRecapitiMode === 'diviso')
<div class="p-4 space-y-4">
{{-- SOTTO-TABELLA CONDOMINO --}}
<div class="rounded-lg border border-amber-200 bg-amber-50/20 overflow-hidden">
<div class="flex items-center justify-between px-3.5 py-2 bg-amber-50 border-b border-amber-200">
<div class="flex items-center gap-2">
<span class="text-sm">🏢</span>
<span class="text-xs font-bold text-amber-950 uppercase tracking-wide">Recapiti Condomino / Aventi Diritto</span>
</div>
<span class="rounded bg-amber-200/80 text-amber-900 font-bold px-2 py-0.5 text-[10px]">
{{ count($recapitiCondomino) }} canali
</span>
</div>
@if(empty($recapitiCondomino))
<div class="p-3 text-center text-xs text-gray-500">Nessun contatto registrato per il Condomino.</div>
@else
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-gray-700 divide-y divide-amber-100/60">
<thead class="bg-amber-50/50 text-[10px] font-bold uppercase tracking-wider text-amber-900/70">
<tr>
<th class="px-3.5 py-2">Canale</th>
<th class="px-3.5 py-2">Etichetta</th>
<th class="px-3.5 py-2">Soggetto</th>
<th class="px-3.5 py-2">Valore Contatto</th>
<th class="px-3.5 py-2 text-center">Stato</th>
<th class="px-3.5 py-2 text-right">Azione</th>
</tr>
</thead>
<tbody class="divide-y divide-amber-100/60 bg-white">
@foreach($recapitiCondomino as $rec)
<tr class="hover:bg-amber-50/40 transition">
<td class="px-3.5 py-2 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-3.5 py-2 font-medium text-gray-800 whitespace-nowrap">{{ $rec['etichetta'] }}</td>
<td class="px-3.5 py-2 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-3.5 py-2 font-mono text-gray-900 font-medium">{{ $rec['valore'] }}</td>
<td class="px-3.5 py-2 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
<span class="text-gray-400 text-[11px]"></span>
@endif
</td>
<td class="px-3.5 py-2 text-right whitespace-nowrap">
@if(!empty($rec['action_url']))
<a href="{{ $rec['action_url'] }}" class="inline-flex items-center gap-1 rounded bg-amber-50 hover:bg-amber-100 border border-amber-200 text-amber-900 font-semibold px-2 py-0.5 text-[11px] transition">
<span>Apri</span>
</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
{{-- SOTTO-TABELLA INQUILINO --}}
<div class="rounded-lg border border-indigo-200 bg-indigo-50/20 overflow-hidden">
<div class="flex items-center justify-between px-3.5 py-2 bg-indigo-50 border-b border-indigo-200">
<div class="flex items-center gap-2">
<span class="text-sm">👤</span>
<span class="text-xs font-bold text-indigo-950 uppercase tracking-wide">Recapiti Inquilino / Conduttore</span>
</div>
<span class="rounded bg-indigo-200/80 text-indigo-900 font-bold px-2 py-0.5 text-[10px]">
{{ count($recapitiInquilino) }} canali
</span>
</div>
@if(empty($recapitiInquilino))
<div class="p-3 text-center text-xs text-gray-500">Nessun contatto o recapito registrato per l'Inquilino.</div>
@else
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-gray-700 divide-y divide-indigo-100/60">
<thead class="bg-indigo-50/50 text-[10px] font-bold uppercase tracking-wider text-indigo-900/70">
<tr>
<th class="px-3.5 py-2">Canale</th>
<th class="px-3.5 py-2">Etichetta</th>
<th class="px-3.5 py-2">Soggetto</th>
<th class="px-3.5 py-2">Valore Contatto</th>
<th class="px-3.5 py-2 text-center">Stato</th>
<th class="px-3.5 py-2 text-right">Azione</th>
</tr>
</thead>
<tbody class="divide-y divide-indigo-100/60 bg-white">
@foreach($recapitiInquilino as $rec)
<tr class="hover:bg-indigo-50/40 transition">
<td class="px-3.5 py-2 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-3.5 py-2 font-medium text-gray-800 whitespace-nowrap">{{ $rec['etichetta'] }}</td>
<td class="px-3.5 py-2 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-3.5 py-2 font-mono text-gray-900 font-medium">{{ $rec['valore'] }}</td>
<td class="px-3.5 py-2 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
<span class="text-gray-400 text-[11px]"></span>
@endif
</td>
<td class="px-3.5 py-2 text-right whitespace-nowrap">
@if(!empty($rec['action_url']))
<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-0.5 text-[11px] transition">
<span>Apri</span>
</a>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
</div>
{{-- VISTA 2: FILTRATA PER CONDOMINO, INQUILINO O TUTTI --}}
@else
@php
$displayedRows = match($currentRecapitiMode) {
'condomino' => $recapitiCondomino,
'inquilino' => $recapitiInquilino,
default => $multiRecapiti,
};
@endphp
<div class="overflow-x-auto">
<table class="w-full text-left text-xs text-gray-700 divide-y divide-gray-100">
<thead class="bg-gray-50 text-[11px] font-bold uppercase tracking-wider text-gray-500">
<tr>
<th class="px-4 py-2.5">Canale</th>
<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>
@endforeach
</tbody>
</table>
</div>
</thead>
<tbody class="divide-y divide-gray-100 bg-white">
@foreach($displayedRows 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
<span class="text-gray-400 text-[11px]"></span>
@endif
</td>
<td class="px-4 py-2.5 text-right whitespace-nowrap">
@if(!empty($rec['action_url']))
<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
</tbody>
</table>
</div>
@endif
@endif
</div>
@endif
@ -610,7 +773,16 @@ class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semi
{{ $rE['gestione'] }}
</span>
</td>
<td class="px-4 py-2 font-medium text-slate-900">{{ $rE['descrizione'] }}</td>
<td class="px-4 py-2 font-medium text-slate-900">
<div class="flex items-center gap-1.5">
@if(str_contains(strtoupper($rE['descrizione'] ?? ''), 'CONGUAGLIO FINALE') || str_contains(strtoupper($rE['descrizione'] ?? ''), ' CF') || ($rE['descrizione'] ?? '') === 'CF')
<span class="inline-flex items-center rounded bg-purple-100 text-purple-800 px-1.5 py-0.5 text-[10px] font-bold">🏁 Conguaglio Finale (CF)</span>
@elseif(str_contains(strtoupper($rE['descrizione'] ?? ''), 'CONGUAGLIO INIZIALE'))
<span class="inline-flex items-center rounded bg-blue-100 text-blue-800 px-1.5 py-0.5 text-[10px] font-bold">🚀 Conguaglio Iniziale</span>
@endif
<span>{{ $rE['descrizione'] }}</span>
</div>
</td>
<td class="px-4 py-2 text-right font-mono font-medium text-slate-900"> {{ number_format($rE['dovuto'], 2, ',', '.') }}</td>
<td class="px-4 py-2 text-right font-mono font-medium text-emerald-700"> {{ number_format($rE['pagato'], 2, ',', '.') }}</td>
<td class="px-4 py-2 font-mono text-slate-700 font-medium">{{ $rE['data_pagamento'] ?? '—' }}</td>

View File

@ -154,3 +154,62 @@
expect($protocollo)->toBeArray();
});
test('unita immobiliare categorizes contacts into condomino and inquilino sub-tables and supports toggle', function () {
$user = User::first() ?: User::factory()->create();
try {
if (method_exists($user, 'assignRole')) {
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
$user->assignRole('admin');
}
} catch (\Throwable $e) {}
Auth::login($user);
$stabile = Stabile::where('codice_stabile', '0021')->first();
expect($stabile)->not->toBeNull();
StabileContext::setActiveStabileId($user, $stabile->id);
$unit = UnitaImmobiliare::where('stabile_id', $stabile->id)->where('scala', 'A')->where('interno', '11')->first();
expect($unit)->not->toBeNull();
request()->merge(['unita_id' => $unit->id]);
$page = new UnitaImmobiliarePage();
$page->mount();
$recapiti = $page->getRecapitiMulticanaleTableProperty();
expect($recapiti)->toBeArray();
foreach ($recapiti as $rec) {
expect($rec)->toHaveKey('tipo_soggetto');
expect(['condomino', 'inquilino'])->toContain($rec['tipo_soggetto']);
}
$page->setRecapitiViewMode('inquilino');
expect($page->recapitiViewMode)->toBe('inquilino');
$page->setRecapitiViewMode('diviso');
expect($page->recapitiViewMode)->toBe('diviso');
});
test('situazione iniziale renders and loads conguagli apertura from dett_tab', function () {
$user = User::first() ?: User::factory()->create();
try {
if (method_exists($user, 'assignRole')) {
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
$user->assignRole('admin');
}
} catch (\Throwable $e) {}
Auth::login($user);
$stabile = Stabile::where('codice_stabile', '0021')->first();
expect($stabile)->not->toBeNull();
StabileContext::setActiveStabileId($user, $stabile->id);
$sitPage = new \App\Filament\Pages\Contabilita\SituazioneIniziale();
$sitPage->mount();
expect($sitPage->tab)->toBe('bilancio');
$sitPage->tab = 'conguagli';
expect($sitPage->tab)->toBe('conguagli');
});