762 lines
30 KiB
PHP
762 lines
30 KiB
PHP
<?php
|
|
namespace App\Services\Contabilita;
|
|
|
|
use App\Models\PersonaUnitaRelazione;
|
|
use App\Models\UnitaImmobiliareNominativo;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class PreventivoOrdinarioPreviewService
|
|
{
|
|
public function build(int $stabileId, int $annoGestione, ?int $gestioneId = null): array
|
|
{
|
|
$gestione = $this->resolveGestione($stabileId, $annoGestione, $gestioneId);
|
|
$gestioneId = $gestione?->id ? (int) $gestione->id : $gestioneId;
|
|
|
|
if (! $gestioneId) {
|
|
return [
|
|
'gestione' => [
|
|
'id' => null,
|
|
'anno' => $annoGestione,
|
|
'denominazione' => (string) ($gestione?->denominazione ?? ''),
|
|
],
|
|
'mesi_rate' => [],
|
|
'preventivo_rows' => [],
|
|
'preventivo_summary' => [],
|
|
'tabella_options' => [],
|
|
'riparto_columns' => [],
|
|
'riparto_rows' => [],
|
|
'focus_unit_id' => null,
|
|
'focus_breakdown' => [],
|
|
'people_audit' => [],
|
|
];
|
|
}
|
|
|
|
$mesiRate = $this->normalizeMonths($gestione?->mesi_rate_ordinaria ?? null);
|
|
$voci = $this->loadVoci($stabileId, $gestioneId);
|
|
$tabellaOptions = $this->buildTabellaOptions($stabileId, $annoGestione);
|
|
$tabellaMap = $tabellaOptions['map'];
|
|
|
|
$summaryByTable = $this->buildTableSummary($voci, $tabellaMap);
|
|
$unitaRows = $this->loadUnitaRows($stabileId);
|
|
$detailRows = $this->loadDetailRows($stabileId, $annoGestione);
|
|
$peopleMap = $this->loadPeopleMap($stabileId, $unitaRows->pluck('id')->all());
|
|
|
|
$matrix = $this->buildMatrixRows($unitaRows, $summaryByTable, $detailRows, $peopleMap, $mesiRate);
|
|
$focusUnitId = $this->resolveFocusUnitId($matrix);
|
|
$focusBreakdown = $this->buildFocusBreakdown($matrix, $focusUnitId);
|
|
|
|
return [
|
|
'gestione' => [
|
|
'id' => $gestioneId,
|
|
'anno' => $annoGestione,
|
|
'denominazione' => (string) ($gestione?->denominazione ?? ''),
|
|
],
|
|
'mesi_rate' => $mesiRate,
|
|
'preventivo_rows' => $voci->map(function (array $row) use ($tabellaMap): array {
|
|
$tabella = $tabellaMap[$row['tabella_millesimale_default_id'] ?? 0] ?? null;
|
|
|
|
return [
|
|
'id' => $row['id'],
|
|
'codice' => $row['codice'],
|
|
'descrizione' => $row['descrizione'],
|
|
'tabella_millesimale_default_id' => $row['tabella_millesimale_default_id'],
|
|
'tabella_codice' => $tabella['codice'] ?? '',
|
|
'tabella_nome' => $tabella['label'] ?? '',
|
|
'importo_default' => $row['importo_default'],
|
|
'importo_consuntivo' => $row['importo_consuntivo'],
|
|
'percentuale_inquilino' => $row['percentuale_inquilino'],
|
|
'percentuale_condomino' => $row['percentuale_condomino'],
|
|
'conto_pd' => $row['conto_pd'],
|
|
'sottoconto_pd' => $row['sottoconto_pd'],
|
|
];
|
|
})->all(),
|
|
'preventivo_summary' => array_values($summaryByTable),
|
|
'tabella_options' => $tabellaOptions['options'],
|
|
'riparto_columns' => array_values(array_map(fn(array $table): array=> [
|
|
'codice' => $table['codice'],
|
|
'label' => $table['codice'],
|
|
'descrizione' => $table['label'],
|
|
], $summaryByTable)),
|
|
'riparto_rows' => $matrix->values()->all(),
|
|
'focus_unit_id' => $focusUnitId,
|
|
'focus_breakdown' => $focusBreakdown,
|
|
'people_audit' => $this->buildPeopleAudit($matrix, $peopleMap),
|
|
];
|
|
}
|
|
|
|
private function resolveGestione(int $stabileId, int $annoGestione, ?int $gestioneId): ?object
|
|
{
|
|
if (! Schema::hasTable('gestioni_contabili')) {
|
|
return null;
|
|
}
|
|
|
|
$query = DB::table('gestioni_contabili')
|
|
->where('stabile_id', $stabileId)
|
|
->where('tipo_gestione', 'ordinaria');
|
|
|
|
if ($gestioneId) {
|
|
$query->where('id', $gestioneId);
|
|
} else {
|
|
$query->where('anno_gestione', $annoGestione)
|
|
->orderByDesc('gestione_attiva')
|
|
->orderByDesc('id');
|
|
}
|
|
|
|
return $query->first();
|
|
}
|
|
|
|
private function normalizeMonths(mixed $raw): array
|
|
{
|
|
$months = [];
|
|
|
|
if (is_array($raw)) {
|
|
$months = $raw;
|
|
} elseif (is_string($raw) && $raw !== '') {
|
|
$decoded = json_decode($raw, true);
|
|
if (is_array($decoded)) {
|
|
$months = $decoded;
|
|
}
|
|
}
|
|
|
|
$months = collect($months)
|
|
->map(fn($month) => (int) $month)
|
|
->filter(fn(int $month) => $month >= 1 && $month <= 12)
|
|
->unique()
|
|
->sort()
|
|
->values()
|
|
->all();
|
|
|
|
return array_map(fn(int $month): array=> [
|
|
'month' => $month,
|
|
'label' => $this->monthLabel($month),
|
|
], $months);
|
|
}
|
|
|
|
private function buildTabellaOptions(int $stabileId, int $annoGestione): array
|
|
{
|
|
$query = DB::table('tabelle_millesimali')
|
|
->where('stabile_id', $stabileId);
|
|
|
|
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
|
|
$query->where('anno_gestione', $annoGestione);
|
|
}
|
|
|
|
$rows = $query
|
|
->orderByRaw('COALESCE(nord, ordine_visualizzazione, ordinamento, 999999)')
|
|
->orderBy('codice_tabella')
|
|
->get([
|
|
'id',
|
|
'codice_tabella',
|
|
'denominazione',
|
|
'totale_millesimi',
|
|
'tipo_calcolo',
|
|
'meta_legacy',
|
|
'nord',
|
|
'ordine_visualizzazione',
|
|
'ordinamento',
|
|
]);
|
|
|
|
$options = [];
|
|
$map = [];
|
|
|
|
foreach ($rows as $row) {
|
|
$metaLegacy = $this->decodeJson($row->meta_legacy ?? null);
|
|
$code = trim((string) ($row->codice_tabella ?? ''));
|
|
$label = trim((string) ($row->denominazione ?? ''));
|
|
$display = $code !== '' ? $code . ' · ' . ($label !== '' ? $label : $code) : ($label !== '' ? $label : 'Tabella');
|
|
$nord = null;
|
|
|
|
foreach ([$row->nord ?? null, $row->ordine_visualizzazione ?? null, $row->ordinamento ?? null] as $orderValue) {
|
|
if (is_numeric($orderValue)) {
|
|
$nord = (int) $orderValue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$options[(int) $row->id] = $display;
|
|
$map[(int) $row->id] = [
|
|
'id' => (int) $row->id,
|
|
'codice' => $code,
|
|
'label' => $label !== '' ? $label : $code,
|
|
'totale_millesimi' => is_numeric($row->totale_millesimi ?? null) ? (float) $row->totale_millesimi : 0.0,
|
|
'tipo_calcolo' => $row->tipo_calcolo,
|
|
'legacy_tipo' => $metaLegacy['tipo'] ?? null,
|
|
'nord' => $nord,
|
|
];
|
|
}
|
|
|
|
return ['options' => $options, 'map' => $map];
|
|
}
|
|
|
|
private function loadVoci(int $stabileId, ?int $gestioneId): Collection
|
|
{
|
|
if (! Schema::hasTable('voci_spesa')) {
|
|
return collect();
|
|
}
|
|
|
|
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $gestioneId) {
|
|
return collect();
|
|
}
|
|
|
|
$query = DB::table('voci_spesa')->where('stabile_id', $stabileId);
|
|
|
|
if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $gestioneId) {
|
|
$query->where('gestione_contabile_id', $gestioneId);
|
|
}
|
|
|
|
if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) {
|
|
$query->where('tipo_gestione', 'ordinaria');
|
|
}
|
|
|
|
$rows = $query
|
|
->orderByRaw('COALESCE(nord, ordinamento, 999999)')
|
|
->orderBy('codice')
|
|
->get([
|
|
'id',
|
|
'codice',
|
|
'descrizione',
|
|
'tabella_millesimale_default_id',
|
|
'importo_default',
|
|
'importo_consuntivo',
|
|
'percentuale_condomino',
|
|
'percentuale_inquilino',
|
|
'conto_pd',
|
|
'sottoconto_pd',
|
|
]);
|
|
|
|
return $rows->map(fn(object $row): array=> [
|
|
'id' => (int) $row->id,
|
|
'codice' => (string) ($row->codice ?? ''),
|
|
'descrizione' => (string) ($row->descrizione ?? ''),
|
|
'tabella_millesimale_default_id' => $row->tabella_millesimale_default_id ? (int) $row->tabella_millesimale_default_id : 0,
|
|
'importo_default' => is_numeric($row->importo_default ?? null) ? (float) $row->importo_default : 0.0,
|
|
'importo_consuntivo' => is_numeric($row->importo_consuntivo ?? null) ? (float) $row->importo_consuntivo : 0.0,
|
|
'percentuale_condomino' => is_numeric($row->percentuale_condomino ?? null) ? (float) $row->percentuale_condomino : 100.0,
|
|
'percentuale_inquilino' => is_numeric($row->percentuale_inquilino ?? null) ? (float) $row->percentuale_inquilino : 0.0,
|
|
'conto_pd' => (string) ($row->conto_pd ?? ''),
|
|
'sottoconto_pd' => (string) ($row->sottoconto_pd ?? ''),
|
|
]);
|
|
}
|
|
|
|
private function buildTableSummary(Collection $voci, array $tabellaMap): array
|
|
{
|
|
return $voci
|
|
->groupBy(fn(array $row): int => (int) ($row['tabella_millesimale_default_id'] ?? 0))
|
|
->map(function (Collection $rows, int $tabellaId) use ($tabellaMap): array {
|
|
$tabella = $tabellaMap[$tabellaId] ?? [
|
|
'codice' => '',
|
|
'label' => '',
|
|
'tipo_calcolo' => null,
|
|
'legacy_tipo' => null,
|
|
'totale_millesimi' => 0.0,
|
|
'nord' => 999999,
|
|
];
|
|
$totalePreventivo = (float) $rows->sum('importo_default');
|
|
$totaleConsuntivo = (float) $rows->sum('importo_consuntivo');
|
|
$quotaInquilini = (float) $rows->sum(function (array $row): float {
|
|
return $row['importo_default'] * (($row['percentuale_inquilino'] ?? 0.0) / 100);
|
|
});
|
|
|
|
return [
|
|
'tabella_id' => $tabellaId,
|
|
'codice' => $tabella['codice'],
|
|
'label' => $tabella['label'],
|
|
'tipo_calcolo' => $tabella['tipo_calcolo'],
|
|
'legacy_tipo' => $tabella['legacy_tipo'],
|
|
'nord' => $tabella['nord'] ?? 999999,
|
|
'totale_millesimi' => $tabella['totale_millesimi'],
|
|
'totale_preventivo' => $totalePreventivo,
|
|
'totale_consuntivo' => $totaleConsuntivo,
|
|
'quota_inquilini' => $quotaInquilini,
|
|
'quota_condomini' => $totalePreventivo - $quotaInquilini,
|
|
'voci' => $rows->values()->all(),
|
|
];
|
|
})
|
|
->sortBy(fn(array $row): string => sprintf('%06d|%s', (int) ($row['nord'] ?? 999999), (string) ($row['codice'] ?? '')))
|
|
->mapWithKeys(fn(array $row): array=> [$row['codice'] => $row])
|
|
->all();
|
|
}
|
|
|
|
private function loadUnitaRows(int $stabileId): Collection
|
|
{
|
|
$query = DB::table('unita_immobiliari')->where('stabile_id', $stabileId);
|
|
|
|
if (Schema::hasColumn('unita_immobiliari', 'deleted_at')) {
|
|
$query->whereNull('deleted_at');
|
|
}
|
|
|
|
return $query
|
|
->orderBy('scala')
|
|
->orderBy('piano')
|
|
->orderBy('interno')
|
|
->orderBy('id')
|
|
->get([
|
|
'id',
|
|
'codice_unita',
|
|
'denominazione',
|
|
'scala',
|
|
'piano',
|
|
'interno',
|
|
'stato_occupazione',
|
|
'legacy_cond_id',
|
|
])
|
|
->map(fn(object $row): array=> [
|
|
'id' => (int) $row->id,
|
|
'label' => $this->buildUnitLabel($row),
|
|
'sort_key' => $this->buildUnitSortKey($row),
|
|
'stato_occupazione' => (string) ($row->stato_occupazione ?? ''),
|
|
'legacy_cond_id' => $row->legacy_cond_id !== null ? (string) $row->legacy_cond_id : null,
|
|
]);
|
|
}
|
|
|
|
private function loadDetailRows(int $stabileId, int $annoGestione): Collection
|
|
{
|
|
if (! Schema::hasTable('dettaglio_millesimi') || ! Schema::hasTable('tabelle_millesimali')) {
|
|
return collect();
|
|
}
|
|
|
|
$query = DB::table('dettaglio_millesimi as dm')
|
|
->join('tabelle_millesimali as tm', 'tm.id', '=', 'dm.tabella_millesimale_id')
|
|
->where('tm.stabile_id', $stabileId);
|
|
|
|
if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) {
|
|
$query->where('tm.anno_gestione', $annoGestione);
|
|
}
|
|
|
|
return $query
|
|
->get([
|
|
'dm.unita_immobiliare_id',
|
|
'tm.codice_tabella',
|
|
'tm.totale_millesimi',
|
|
'dm.millesimi',
|
|
'dm.valore_prev',
|
|
'dm.ruolo_legacy',
|
|
])
|
|
->map(fn(object $row): array=> [
|
|
'unit_id' => (int) $row->unita_immobiliare_id,
|
|
'tabella_codice' => (string) ($row->codice_tabella ?? ''),
|
|
'totale_millesimi' => is_numeric($row->totale_millesimi ?? null) ? (float) $row->totale_millesimi : 0.0,
|
|
'millesimi' => is_numeric($row->millesimi ?? null) ? (float) $row->millesimi : 0.0,
|
|
'valore_prev' => is_numeric($row->valore_prev ?? null) ? (float) $row->valore_prev : 0.0,
|
|
'ruolo_legacy' => strtoupper(trim((string) ($row->ruolo_legacy ?? ''))),
|
|
]);
|
|
}
|
|
|
|
private function buildMatrixRows(Collection $unitaRows, array $summaryByTable, Collection $detailRows, array $peopleMap, array $mesiRate): Collection
|
|
{
|
|
$detailsByUnitTable = $detailRows->groupBy(fn(array $row): string => $row['unit_id'] . '|' . $row['tabella_codice']);
|
|
|
|
return $unitaRows->map(function (array $unit) use ($summaryByTable, $detailsByUnitTable, $peopleMap, $mesiRate): array {
|
|
$columns = [];
|
|
$breakdown = [];
|
|
$totaleUnit = 0.0;
|
|
$totaleCondomini = 0.0;
|
|
$totaleInquilini = 0.0;
|
|
$owners = $peopleMap[$unit['id']]['owners'] ?? [];
|
|
$tenants = $peopleMap[$unit['id']]['tenants'] ?? [];
|
|
|
|
foreach ($summaryByTable as $tabellaCode => $table) {
|
|
$detail = collect($detailsByUnitTable->get($unit['id'] . '|' . $tabellaCode, []));
|
|
$computed = $this->computeUnitTableAllocation($table, $detail);
|
|
|
|
$columns[$tabellaCode] = $computed['totale'];
|
|
$breakdown[$tabellaCode] = $computed;
|
|
$totaleUnit += $computed['totale'];
|
|
$totaleCondomini += $computed['condomini'];
|
|
$totaleInquilini += $computed['inquilini'];
|
|
}
|
|
|
|
return [
|
|
'unit_id' => $unit['id'],
|
|
'unit_label' => $unit['label'],
|
|
'stato_occupazione' => $unit['stato_occupazione'],
|
|
'people_summary' => $peopleMap[$unit['id']]['summary'] ?? 'Nessun soggetto collegato',
|
|
'owners' => $owners,
|
|
'tenants' => $tenants,
|
|
'legacy_cond_id' => $unit['legacy_cond_id'],
|
|
'columns' => $columns,
|
|
'table_breakdown' => $breakdown,
|
|
'totale_unita' => $totaleUnit,
|
|
'totale_condomini' => $totaleCondomini,
|
|
'totale_inquilini' => $totaleInquilini,
|
|
'rate' => $this->splitAcrossMonths($totaleUnit, $mesiRate),
|
|
];
|
|
})->sort(function (array $left, array $right): int {
|
|
return strnatcasecmp((string) ($left['sort_key'] ?? $left['unit_label'] ?? ''), (string) ($right['sort_key'] ?? $right['unit_label'] ?? ''));
|
|
})->values();
|
|
}
|
|
|
|
private function computeUnitTableAllocation(array $table, Collection $detailRows): array
|
|
{
|
|
$totaleTabella = (float) ($table['totale_preventivo'] ?? 0.0);
|
|
$tabellaMillesimi = (float) ($table['totale_millesimi'] ?? 0.0);
|
|
|
|
$millesimiUnita = (float) $detailRows
|
|
->filter(fn(array $row): bool => $row['ruolo_legacy'] !== 'I')
|
|
->sum('millesimi');
|
|
|
|
$ratio = $tabellaMillesimi > 0 ? ($millesimiUnita / $tabellaMillesimi) : 0.0;
|
|
$impProp = (float) $detailRows
|
|
->filter(fn(array $row): bool => $row['ruolo_legacy'] !== 'I')
|
|
->sum('valore_prev');
|
|
$impInq = (float) $detailRows
|
|
->filter(fn(array $row): bool => $row['ruolo_legacy'] === 'I')
|
|
->sum('valore_prev');
|
|
$impTot = $impProp + $impInq;
|
|
$hasImporti = abs($impTot) > 0.00001;
|
|
$useImportiSplit = $hasImporti && abs($impInq) > 0.00001;
|
|
$quotaUnit = $hasImporti ? $impTot : ($totaleTabella * $ratio);
|
|
|
|
$quotaCondomini = 0.0;
|
|
$quotaInquilini = 0.0;
|
|
|
|
if ($useImportiSplit) {
|
|
$quotaCondomini = $impProp;
|
|
$quotaInquilini = $impInq;
|
|
} else {
|
|
foreach ($table['voci'] as $voce) {
|
|
$share = $totaleTabella != 0.0 ? ((float) $voce['importo_default'] / $totaleTabella) : 0.0;
|
|
$quotaVoce = $hasImporti ? ($quotaUnit * $share) : ((float) $voce['importo_default'] * $ratio);
|
|
$quotaInqVoce = $quotaVoce * (((float) ($voce['percentuale_inquilino'] ?? 0.0)) / 100);
|
|
$quotaCondomini += $quotaVoce - $quotaInqVoce;
|
|
$quotaInquilini += $quotaInqVoce;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'totale' => $quotaUnit,
|
|
'condomini' => $quotaCondomini,
|
|
'inquilini' => $quotaInquilini,
|
|
'millesimi_unita' => $millesimiUnita,
|
|
'millesimi_tabella' => $tabellaMillesimi,
|
|
];
|
|
}
|
|
|
|
private function loadPeopleMap(int $stabileId, array $unitIds): array
|
|
{
|
|
$map = [];
|
|
|
|
if ($unitIds === []) {
|
|
return $map;
|
|
}
|
|
|
|
if (Schema::hasTable('persone_unita_relazioni')) {
|
|
$relations = PersonaUnitaRelazione::query()
|
|
->with('persona')
|
|
->whereIn('unita_id', $unitIds)
|
|
->attive()
|
|
->get();
|
|
|
|
foreach ($relations as $relation) {
|
|
$unitId = (int) $relation->unita_id;
|
|
$role = strtoupper((string) ($relation->ruolo_rate ?: PersonaUnitaRelazione::deriveRuoloRate($relation->tipo_relazione)));
|
|
if (! in_array($role, ['C', 'I'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$map[$unitId]['relations'][$role][] = [
|
|
'name' => (string) ($relation->persona?->nome_display ?? $relation->persona?->nome_completo ?? 'Persona'),
|
|
'quota' => is_numeric($relation->quota_relazione ?? null) ? (float) $relation->quota_relazione : null,
|
|
'source' => 'relazione_attiva',
|
|
];
|
|
}
|
|
}
|
|
|
|
if (Schema::hasTable('unita_immobiliare_nominativi')) {
|
|
$today = now()->toDateString();
|
|
$legacyRows = UnitaImmobiliareNominativo::query()
|
|
->where('stabile_id', $stabileId)
|
|
->whereIn('unita_immobiliare_id', $unitIds)
|
|
->where(function ($query) use ($today): void {
|
|
$query->whereNull('data_inizio')->orWhere('data_inizio', '<=', $today);
|
|
})
|
|
->where(function ($query) use ($today): void {
|
|
$query->whereNull('data_fine')->orWhere('data_fine', '>=', $today);
|
|
})
|
|
->get();
|
|
|
|
foreach ($legacyRows as $row) {
|
|
$unitId = (int) $row->unita_immobiliare_id;
|
|
$role = strtoupper((string) ($row->ruolo ?? ''));
|
|
if (! in_array($role, ['C', 'I'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$map[$unitId]['legacy'][$role][] = [
|
|
'name' => (string) ($row->nominativo ?? 'Nominativo legacy'),
|
|
'quota' => is_numeric($row->percentuale ?? null) ? (float) $row->percentuale : null,
|
|
'source' => (string) ($row->fonte ?? 'legacy'),
|
|
];
|
|
}
|
|
}
|
|
|
|
foreach ($unitIds as $unitId) {
|
|
$owners = $map[$unitId]['relations']['C'] ?? $map[$unitId]['legacy']['C'] ?? [];
|
|
$tenants = $map[$unitId]['relations']['I'] ?? $map[$unitId]['legacy']['I'] ?? [];
|
|
|
|
$ownerNames = collect($owners)->pluck('name')->filter()->values()->all();
|
|
$tenantNames = collect($tenants)->pluck('name')->filter()->values()->all();
|
|
|
|
$summary = [];
|
|
if ($ownerNames !== []) {
|
|
$summary[] = 'Prop: ' . implode(', ', $ownerNames);
|
|
}
|
|
if ($tenantNames !== []) {
|
|
$summary[] = 'Inq: ' . implode(', ', $tenantNames);
|
|
}
|
|
|
|
$map[$unitId]['owners'] = $owners;
|
|
$map[$unitId]['tenants'] = $tenants;
|
|
$map[$unitId]['summary'] = $summary === [] ? 'Nessun soggetto collegato' : implode(' · ', $summary);
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
private function buildPeopleAudit(Collection $matrix, array $peopleMap): array
|
|
{
|
|
return $matrix->map(function (array $row) use ($peopleMap): array {
|
|
$people = $peopleMap[$row['unit_id']] ?? ['owners' => [], 'tenants' => [], 'relations' => [], 'legacy' => []];
|
|
|
|
$owners = $people['owners'] ?? [];
|
|
$tenants = $people['tenants'] ?? [];
|
|
$ownerQuota = $this->sumDeclaredQuota($owners);
|
|
$tenantQuota = $this->sumDeclaredQuota($tenants);
|
|
$flags = [];
|
|
|
|
if ($owners === []) {
|
|
$flags[] = 'Manca proprietario attivo';
|
|
}
|
|
if ($row['stato_occupazione'] === 'occupata_inquilino' && $tenants === []) {
|
|
$flags[] = 'Unità occupata da inquilino senza inquilino attivo';
|
|
}
|
|
if ($owners !== [] && $ownerQuota !== null && abs($ownerQuota - 100.0) > 0.01) {
|
|
$flags[] = 'Quote proprietari ' . number_format($ownerQuota, 2, ',', '.') . '%';
|
|
}
|
|
if (($people['relations']['C'] ?? []) === [] && ($people['legacy']['C'] ?? []) !== []) {
|
|
$flags[] = 'Solo nominativi legacy per proprietari';
|
|
}
|
|
if (($people['relations']['I'] ?? []) === [] && ($people['legacy']['I'] ?? []) !== []) {
|
|
$flags[] = 'Solo nominativi legacy per inquilini';
|
|
}
|
|
|
|
return [
|
|
'unit_id' => $row['unit_id'],
|
|
'unit_label' => $row['unit_label'],
|
|
'owners' => implode(', ', array_map(fn(array $item): string => $item['name'], $owners)),
|
|
'tenants' => implode(', ', array_map(fn(array $item): string => $item['name'], $tenants)),
|
|
'owner_quota' => $ownerQuota,
|
|
'tenant_quota' => $tenantQuota,
|
|
'flags' => $flags,
|
|
];
|
|
})->all();
|
|
}
|
|
|
|
private function buildFocusBreakdown(Collection $matrix, ?int $focusUnitId): array
|
|
{
|
|
if (! $focusUnitId) {
|
|
return [];
|
|
}
|
|
|
|
$row = $matrix->firstWhere('unit_id', $focusUnitId);
|
|
if (! is_array($row)) {
|
|
return [];
|
|
}
|
|
|
|
$subjects = [];
|
|
$ownerShares = $this->normalizePeopleShares($row['table_breakdown'], 'owners');
|
|
$tenantShares = $this->normalizePeopleShares($row['table_breakdown'], 'tenants');
|
|
|
|
foreach ($ownerShares as $share) {
|
|
$subjects[] = $share;
|
|
}
|
|
foreach ($tenantShares as $share) {
|
|
$subjects[] = $share;
|
|
}
|
|
|
|
return $subjects;
|
|
}
|
|
|
|
private function normalizePeopleShares(array $tableBreakdown, string $bucket): array
|
|
{
|
|
$first = reset($tableBreakdown);
|
|
if (! is_array($first) || ! isset($first[$bucket]) || ! is_array($first[$bucket])) {
|
|
return [];
|
|
}
|
|
|
|
$people = $first[$bucket];
|
|
$totalBase = $bucket === 'owners' ? 'condomini' : 'inquilini';
|
|
$shares = $this->normalizeShares($people);
|
|
$rows = [];
|
|
|
|
foreach ($shares as $person) {
|
|
$totale = 0.0;
|
|
$columns = [];
|
|
|
|
foreach ($tableBreakdown as $code => $values) {
|
|
$base = (float) ($values[$totalBase] ?? 0.0);
|
|
$quota = $base * $person['share'];
|
|
$columns[$code] = $quota;
|
|
$totale += $quota;
|
|
}
|
|
|
|
$rows[] = [
|
|
'name' => $person['name'],
|
|
'role' => $bucket === 'owners' ? 'Condomino' : 'Inquilino',
|
|
'quota_percent' => $person['share'] * 100,
|
|
'columns' => $columns,
|
|
'totale' => $totale,
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
private function normalizeShares(array $people): array
|
|
{
|
|
if ($people === []) {
|
|
return [];
|
|
}
|
|
|
|
$declared = collect($people)->sum(fn(array $row): float => is_numeric($row['quota'] ?? null) ? (float) $row['quota'] : 0.0);
|
|
$count = count($people);
|
|
|
|
return array_map(function (array $row) use ($declared, $count): array {
|
|
$share = $declared > 0
|
|
? (((float) ($row['quota'] ?? 0.0)) / $declared)
|
|
: (1 / max($count, 1));
|
|
|
|
return [
|
|
'name' => $row['name'],
|
|
'share' => $share,
|
|
];
|
|
}, $people);
|
|
}
|
|
|
|
private function sumDeclaredQuota(array $people): ?float
|
|
{
|
|
$declared = collect($people)
|
|
->filter(fn(array $row): bool => is_numeric($row['quota'] ?? null))
|
|
->sum(fn(array $row): float => (float) $row['quota']);
|
|
|
|
return $declared > 0 ? $declared : null;
|
|
}
|
|
|
|
private function splitAcrossMonths(float $total, array $mesiRate): array
|
|
{
|
|
$count = count($mesiRate);
|
|
if ($count === 0) {
|
|
return [];
|
|
}
|
|
|
|
$base = round($total / $count, 2);
|
|
$out = [];
|
|
$allocated = 0.0;
|
|
|
|
foreach ($mesiRate as $index => $month) {
|
|
$amount = $index === ($count - 1) ? round($total - $allocated, 2) : $base;
|
|
$allocated += $amount;
|
|
$out[] = [
|
|
'month' => $month['month'],
|
|
'label' => $month['label'],
|
|
'amount' => $amount,
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function resolveFocusUnitId(Collection $matrix): ?int
|
|
{
|
|
$first = $matrix->first();
|
|
|
|
return is_array($first) ? (int) ($first['unit_id'] ?? 0) : null;
|
|
}
|
|
|
|
private function buildUnitLabel(object $row): string
|
|
{
|
|
$parts = [];
|
|
|
|
$scala = trim((string) ($row->scala ?? ''));
|
|
$piano = trim((string) ($row->piano ?? ''));
|
|
$interno = trim((string) ($row->interno ?? ''));
|
|
$denominazione = trim((string) ($row->denominazione ?? ''));
|
|
$codice = trim((string) ($row->codice_unita ?? ''));
|
|
|
|
if ($scala !== '') {
|
|
$parts[] = 'Scala ' . $scala;
|
|
}
|
|
if ($piano !== '') {
|
|
$parts[] = 'Piano ' . $piano;
|
|
}
|
|
if ($interno !== '') {
|
|
$parts[] = 'Int. ' . $interno;
|
|
}
|
|
|
|
$label = implode(' · ', $parts);
|
|
if ($label === '') {
|
|
$label = $denominazione !== '' ? $denominazione : ($codice !== '' ? $codice : 'Unità #' . $row->id);
|
|
}
|
|
|
|
if ($codice !== '') {
|
|
$label .= ' [' . $codice . ']';
|
|
}
|
|
|
|
return $label;
|
|
}
|
|
|
|
private function buildUnitSortKey(object $row): string
|
|
{
|
|
return implode('|', [
|
|
$this->normalizeNaturalToken((string) ($row->scala ?? '')),
|
|
$this->normalizeNaturalToken((string) ($row->piano ?? '')),
|
|
$this->normalizeNaturalToken((string) ($row->interno ?? '')),
|
|
$this->normalizeNaturalToken((string) ($row->denominazione ?? '')),
|
|
str_pad((string) ((int) ($row->id ?? 0)), 10, '0', STR_PAD_LEFT),
|
|
]);
|
|
}
|
|
|
|
private function normalizeNaturalToken(string $value): string
|
|
{
|
|
$normalized = strtoupper(trim($value));
|
|
|
|
return preg_replace('/[\s\.\-\/]+/', '', $normalized) ?? '';
|
|
}
|
|
|
|
private function monthLabel(int $month): string
|
|
{
|
|
return [
|
|
1 => 'Gen',
|
|
2 => 'Feb',
|
|
3 => 'Mar',
|
|
4 => 'Apr',
|
|
5 => 'Mag',
|
|
6 => 'Giu',
|
|
7 => 'Lug',
|
|
8 => 'Ago',
|
|
9 => 'Set',
|
|
10 => 'Ott',
|
|
11 => 'Nov',
|
|
12 => 'Dic',
|
|
][$month] ?? (string) $month;
|
|
}
|
|
|
|
private function decodeJson(mixed $value): array
|
|
{
|
|
if (is_array($value)) {
|
|
return $value;
|
|
}
|
|
|
|
if (is_string($value) && $value !== '') {
|
|
$decoded = json_decode($value, true);
|
|
if (is_array($decoded)) {
|
|
return $decoded;
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|