hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } protected int $stabileId = 0; protected ?string $backUrl = null; public string $millesimiGruppo = 'ordinarie'; public ?int $unitaId = null; public ?array $unitaPickerData = []; public string $unitaVisibilita = 'attive'; /** * @var array */ protected array $unitaOptionsCache = []; protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-building-office-2'; protected static ?string $navigationLabel = 'Unità immobiliare'; protected static ?string $title = 'Unità immobiliare'; protected static UnitEnum|string|null $navigationGroup = 'Stabile'; protected string $view = 'filament.pages.unita-immobiliare'; protected static ?string $slug = 'unita-immobiliare'; public ?UnitaImmobiliare $unita = null; public string $tab = 'riepilogo'; public array $millesimiPerTabella = []; public array $dirittiProprieta = []; public array $relazioniPerTipo = []; public array $nominativiStorici = []; public array $recapitiServizio = []; protected ?object $legacyCondominRow = null; public array $ripartizioniPerTabella = []; public array $preventiviPerTabella = []; public array $totaliPerGestione = []; public array $rateEmessePerCategoria = []; /** Estratto conto compatto: rate emesse */ public array $estrattoCompattoRateRows = []; /** Estratto conto compatto: incassi */ public array $estrattoCompattoIncassi = []; /** Conguagli iniziali (legacy) */ public array $estrattoConguagliIniziali = []; /** * Sezione estratto conto: quale categoria mostrare (condomini|inquilini) */ public string $estrattoTipo = 'condomini'; /** * @var array */ protected array $ownerByUnitaLocal = []; public function getBackUrl(): ?string { $candidate = request()->query('back'); if (! is_string($candidate) || trim($candidate) === '') { return null; } return $candidate; } public function mount(): void { $user = Auth::user(); if (! $user instanceof User) { abort(403); } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { abort(403); } $this->stabileId = (int) $stabileId; $this->unitaVisibilita = $this->normalizeUnitaVisibilita(request()->query('visibilita')); $this->refreshUnitaOptions(); $this->unitaId = $this->pickUnitaId(request()->integer('unita_id')); $this->backUrl = $this->getBackUrl(); $this->getSchema('unitaPicker')?->fill([ 'unita_id' => $this->unitaId, ]); $this->loadUnita(); } public function hydrate(): void { $user = Auth::user(); if (! $user instanceof User) { return; } $stabileId = StabileContext::resolveActiveStabileId($user); if ($stabileId) { $this->stabileId = (int) $stabileId; } if (empty($this->unitaOptionsCache)) { $this->refreshUnitaOptions(); } if (empty($this->unitaPickerData)) { $this->getSchema('unitaPicker')?->fill([ 'unita_id' => $this->unitaId, ]); } if ($this->unitaId) { $this->loadUnita(); } } public function dehydrate(): void { $this->unita = null; $this->millesimiPerTabella = []; $this->dirittiProprieta = []; $this->relazioniPerTipo = []; $this->nominativiStorici = []; $this->recapitiServizio = []; $this->ripartizioniPerTabella = []; $this->preventiviPerTabella = []; $this->totaliPerGestione = []; $this->rateEmessePerCategoria = []; $this->estrattoCompattoRateRows = []; $this->estrattoCompattoIncassi = []; $this->estrattoConguagliIniziali = []; $this->legacyCondominRow = null; } public function unitaPicker(Schema $schema): Schema { return $schema ->statePath('unitaPickerData') ->components([ Select::make('unita_id') ->label('Unità') ->hiddenLabel() ->placeholder('Cerca unità…') ->options(fn(): array=> $this->getUnitaOptions()) ->searchable() ->preload() ->native(false) ->live() ->afterStateUpdated(function ($state): void { $id = is_numeric($state) ? (int) $state : null; $this->unitaId = $this->pickUnitaId($id); if (! $this->unitaId) { return; } $this->redirect($this->makeSelfUrl($this->unitaId)); }), ]); } public function updatedUnitaId(): void { $this->unitaId = $this->pickUnitaId($this->unitaId); if (! $this->unitaId) { $this->loadUnita(); return; } $this->redirect($this->makeSelfUrl($this->unitaId)); } protected function getHeaderActions(): array { $prevId = $this->getPrevUnitaId(); $nextId = $this->getNextUnitaId(); return [ Action::make('modifica_unita') ->label('Modifica unità') ->icon('heroicon-o-pencil-square') ->modalWidth('5xl') ->visible(fn(): bool => (bool) $this->unitaId && $this->unita instanceof UnitaImmobiliare) ->fillForm(fn(): array=> $this->getEditUnitaFormData()) ->form([ TextInput::make('codice_unita') ->label('Codice unità') ->maxLength(100) ->columnSpan(3), TextInput::make('denominazione') ->label('Denominazione') ->maxLength(255) ->columnSpan(5), TextInput::make('tipo_unita') ->label('Tipologia') ->maxLength(100) ->columnSpan(2), TextInput::make('categoria_catastale') ->label('Categoria catastale') ->maxLength(20) ->columnSpan(2), TextInput::make('scala') ->label('Scala') ->maxLength(50) ->columnSpan(2), TextInput::make('piano') ->label('Piano') ->maxLength(50) ->columnSpan(2), TextInput::make('interno') ->label('Interno') ->maxLength(50) ->columnSpan(2), TextInput::make('stato_occupazione') ->label('Stato occupazione') ->maxLength(100) ->columnSpan(3), TextInput::make('utilizzo_attuale') ->label('Utilizzo attuale') ->maxLength(100) ->columnSpan(3), Toggle::make('attiva') ->label('Unità attiva') ->columnSpan(2), Textarea::make('note') ->label('Note') ->rows(4) ->columnSpanFull(), ]) ->action(function (array $data): void { $this->saveEditedUnita($data); }), Action::make('prev_unita') ->label('Precedente') ->icon('heroicon-o-arrow-left') ->color('gray') ->visible(fn(): bool => (bool) $this->getPrevUnitaId()) ->url(fn(): string => $this->makeSelfUrl($prevId)), Action::make('next_unita') ->label('Successiva') ->icon('heroicon-o-arrow-right') ->color('gray') ->visible(fn(): bool => (bool) $this->getNextUnitaId()) ->url(fn(): string => $this->makeSelfUrl($nextId)), Action::make('storico_ruoli') ->label('Storico ruoli') ->icon('heroicon-o-clock') ->modalWidth('7xl') ->visible(fn(): bool => (bool) $this->unitaId) ->form([ Repeater::make('ruoli') ->label('Storico proprietari / inquilini') ->defaultItems(0) ->reorderable(true) ->schema([ Hidden::make('id'), Select::make('rubrica_id') ->label('Persona') ->required() ->searchable() ->getSearchResultsUsing(function (string $search): array { return RubricaUniversale::query() ->ricerca($search) ->limit(30) ->get() ->mapWithKeys(fn(RubricaUniversale $r) => [ $r->id => $r->nome_completo . ($r->email ? (' · ' . $r->email) : ''), ]) ->all(); }) ->getOptionLabelUsing(function ($value): ?string { if (! $value) { return null; } $r = RubricaUniversale::query()->find($value); return $r ? ($r->nome_completo . ($r->email ? (' · ' . $r->email) : '')) : null; }) ->columnSpan(5), DatePicker::make('data_inizio') ->label('Dal') ->columnSpan(3), DatePicker::make('data_fine') ->label('Fino al') ->columnSpan(3), Select::make('ruolo') ->label('Ruolo') ->options($this->getRuoliLabels()) ->columnSpan(3), Toggle::make('attivo') ->label('Attivo') ->columnSpan(2), ]) ->columns(9), ]), Action::make('recapiti_email') ->label('Recapiti email') ->icon('heroicon-o-envelope') ->modalWidth('7xl') ->visible(fn(): bool => (bool) $this->unitaId) ->form([ Repeater::make('recapiti') ->label('Recapiti email per servizio') ->defaultItems(0) ->reorderable(true) ->schema([ Hidden::make('id'), Select::make('tipo_servizio') ->label('Servizio') ->options(UnitaRecapitoServizio::serviceLabels()) ->native(false) ->required() ->columnSpan(3), Select::make('persona_id') ->label('Persona collegata') ->options(fn(): array=> $this->getPersonaRecapitoOptions()) ->searchable() ->preload() ->native(false) ->columnSpan(4), TextInput::make('email') ->label('Email da usare') ->email() ->required() ->placeholder('nome@example.it') ->helperText('Scegli la persona collegata e usa una delle email mostrate nel relativo riepilogo.') ->columnSpan(5), TextInput::make('etichetta') ->label('Etichetta') ->maxLength(120) ->columnSpan(4), TextInput::make('note') ->label('Note') ->maxLength(255) ->columnSpan(4), Toggle::make('is_default') ->label('Predefinita') ->default(true) ->columnSpan(2), Toggle::make('attivo') ->label('Attiva') ->default(true) ->columnSpan(2), ]) ->columns(12), ]) ->fillForm(fn(): array=> ['recapiti' => $this->getRecapitiServizioFormRows()]) ->action(function (array $data): void { $this->saveRecapitiServizioOverrides($data['recapiti'] ?? []); $this->loadUnita(); \Filament\Notifications\Notification::make() ->title('Recapiti email aggiornati') ->success() ->send(); }), ]; } protected function getRuoliLabels(): array { return [ 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'proprietario' => 'Proprietario', 'fornitore' => 'Fornitore', 'altro' => 'Altro', ]; } private function getPersonaRecapitoOptions(): array { if (! $this->unita) { return []; } return $this->unita->relazioniPersoneAttive ->map(fn($relazione) => $relazione->persona) ->filter(fn($persona) => $persona instanceof Persona) ->unique(fn(Persona $persona) => (int) $persona->id) ->mapWithKeys(function (Persona $persona): array { $emails = $this->extractPersonaEmails($persona); $label = $persona->nome_completo; if ($emails !== []) { $label .= ' · ' . implode(', ', $emails); } return [(int) $persona->id => $label]; }) ->all(); } private function getRecapitiServizioFormRows(): array { if (! $this->unita) { return []; } return $this->unita->unitaRecapitiServizio ->sortBy(['tipo_servizio', 'ordine', 'id']) ->values() ->map(function (UnitaRecapitoServizio $row): array { return [ 'id' => (int) $row->id, 'tipo_servizio' => (string) $row->tipo_servizio, 'persona_id' => $row->persona_id ? (int) $row->persona_id : null, 'email' => (string) $row->email, 'etichetta' => (string) ($row->etichetta ?? ''), 'note' => (string) ($row->note ?? ''), 'is_default' => (bool) $row->is_default, 'attivo' => (bool) $row->attivo, ]; }) ->all(); } private function saveRecapitiServizioOverrides(array $rows): void { if (! $this->unita) { return; } $labels = UnitaRecapitoServizio::serviceLabels(); DB::transaction(function () use ($rows, $labels): void { UnitaRecapitoServizio::query() ->where('unita_id', (int) $this->unita->id) ->delete(); $ordine = 1; foreach ($rows as $row) { $tipoServizio = trim((string) ($row['tipo_servizio'] ?? '')); $email = mb_strtolower(trim((string) ($row['email'] ?? ''))); if ($tipoServizio === '' || $email === '' || ! array_key_exists($tipoServizio, $labels)) { continue; } UnitaRecapitoServizio::create([ 'unita_id' => (int) $this->unita->id, 'persona_id' => isset($row['persona_id']) && is_numeric($row['persona_id']) ? (int) $row['persona_id'] : null, 'tipo_servizio' => $tipoServizio, 'email' => $email, 'etichetta' => $this->normalizeOptionalString($row['etichetta'] ?? null) ?: ($labels[$tipoServizio] ?? null), 'note' => $this->normalizeOptionalString($row['note'] ?? null), 'ordine' => $ordine++, 'is_default' => (bool) ($row['is_default'] ?? true), 'attivo' => (bool) ($row['attivo'] ?? true), 'sorgente' => 'manuale', 'last_confirmed_at' => now(), ]); } }); } protected function makeSelfUrl(?int $unitaId = null): string { $base = static::getUrl(panel: 'admin-filament'); $query = []; if ($unitaId) { $query['unita_id'] = (int) $unitaId; } if (is_string($this->backUrl) && trim($this->backUrl) !== '') { $query['back'] = $this->backUrl; } return $base . (empty($query) ? '' : ('?' . http_build_query($query))); } protected function getPrevUnitaId(): ?int { if (! $this->unitaId || empty($this->unitaOptionsCache)) { return null; } $ids = array_map('intval', array_keys($this->unitaOptionsCache)); $index = array_search((int) $this->unitaId, $ids, true); if ($index === false || $index <= 0) { return null; } return (int) $ids[$index - 1]; } protected function getNextUnitaId(): ?int { if (! $this->unitaId || empty($this->unitaOptionsCache)) { return null; } $ids = array_map('intval', array_keys($this->unitaOptionsCache)); $index = array_search((int) $this->unitaId, $ids, true); if ($index === false) { return null; } $nextIndex = $index + 1; if (! isset($ids[$nextIndex])) { return null; } return (int) $ids[$nextIndex]; } public function getPrevUnitaUrl(): ?string { $id = $this->getPrevUnitaId(); return $id ? $this->makeSelfUrl($id) : null; } public function getNextUnitaUrl(): ?string { $id = $this->getNextUnitaId(); return $id ? $this->makeSelfUrl($id) : null; } public function setTab(string $tab): void { $this->tab = $tab; } public function setUnitaVisibilita(string $visibilita): void { $this->unitaVisibilita = $this->normalizeUnitaVisibilita($visibilita); $this->refreshUnitaOptions(); $this->unitaId = $this->pickUnitaId($this->unitaId); $this->getSchema('unitaPicker')?->fill([ 'unita_id' => $this->unitaId, ]); if ($this->unitaId) { $this->redirect($this->makeSelfUrl($this->unitaId)); return; } $this->loadUnita(); } protected function refreshUnitaOptions(): void { $unita = $this->buildUnitaIndexQuery() ->orderBy('palazzina_id') ->orderBy('scala') ->orderBy('piano') ->orderBy('id') ->get(['id', 'palazzina_id', 'codice_unita', 'denominazione', 'scala', 'piano', 'interno', 'deleted_at']); $unita = $unita->filter(function (UnitaImmobiliare $u): bool { $denominazione = trim((string) ($u->denominazione ?? '')); $scala = trim((string) ($u->scala ?? '')); $interno = trim((string) ($u->interno ?? '')); $piano = $u->piano; $codice = trim((string) ($u->codice_unita ?? '')); $isEmptyCore = $denominazione === '' && $scala === '' && $interno === '' && $piano === null; if ($isEmptyCore && empty($u->palazzina_id)) { return false; } if ($codice === '0019-A-3-C5') { return false; } return true; })->values(); $ownerByUnita = []; if (DbSchema::hasTable('proprieta')) { try { $ownerByUnita = Proprieta::query() ->select('unita_immobiliare_id', 'soggetto_id') ->whereIn('unita_immobiliare_id', $unita->pluck('id')) ->get() ->groupBy('unita_immobiliare_id') ->map(function ($rows) { $ids = $rows->pluck('soggetto_id') ->filter(fn($v) => is_numeric($v) && (int) $v > 0) ->map(fn($v) => (int) $v) ->values() ->all(); if (empty($ids)) { return null; } $nome = DB::table('soggetti') ->whereIn('id', $ids) ->select('id', 'ragione_sociale', 'nome', 'cognome') ->get() ->map(function ($s) { $nome = trim((string) ($s->ragione_sociale ?? '')); if ($nome === '') { $nome = trim((string) ($s->nome ?? '') . ' ' . (string) ($s->cognome ?? '')); } return $nome !== '' ? $nome : null; }) ->filter() ->values() ->first(); return $nome ?: null; }) ->filter() ->all(); } catch (\Throwable $e) { $ownerByUnita = []; } } $sorted = $unita->sort(function (UnitaImmobiliare $a, UnitaImmobiliare $b): int { $pa = (int) ($a->palazzina_id ?? 0); $pb = (int) ($b->palazzina_id ?? 0); if ($pa !== $pb) { return $pa <=> $pb; } $sa = strtoupper(trim((string) ($a->scala ?? ''))); $sb = strtoupper(trim((string) ($b->scala ?? ''))); if ($sa !== $sb) { return strcmp($sa, $sb); } $pia = is_null($a->piano) ? 0 : (int) $a->piano; $pib = is_null($b->piano) ? 0 : (int) $b->piano; if ($pia !== $pib) { return $pia <=> $pib; } $ka = $this->internoSortKey($a->interno !== null ? (string) $a->interno : null); $kb = $this->internoSortKey($b->interno !== null ? (string) $b->interno : null); if ($ka[0] !== $kb[0]) { return strcmp($ka[0], $kb[0]); } if ($ka[1] !== $kb[1]) { return $ka[1] <=> $kb[1]; } if ($ka[2] !== $kb[2]) { return strnatcasecmp($ka[2], $kb[2]); } return ((int) $a->id) <=> ((int) $b->id); }); $this->ownerByUnitaLocal = $ownerByUnita; $this->unitaOptionsCache = $sorted ->mapWithKeys(function (UnitaImmobiliare $u) { $codice = $u->codice_unita ?: ('ID ' . $u->id); $owner = $this->ownerByUnitaLocal[(int) $u->id] ?? null; $label = trim($codice); if (is_string($owner) && trim($owner) !== '') { $label .= ' — ' . trim($owner); } if ($u->trashed()) { $label .= ' [archiviata]'; } return [(int) $u->id => $label]; }) ->all(); } /** * @return array */ public function getUnitaOptions(): array { if (empty($this->unitaOptionsCache)) { $this->refreshUnitaOptions(); } return $this->unitaOptionsCache; } public function setEstrattoTipo(string $tipo): void { $tipo = strtolower(trim($tipo)); if (! in_array($tipo, ['condomini', 'inquilini'], true)) { return; } $this->estrattoTipo = $tipo; } /** * @return array{0:string,1:int,2:string} */ protected function internoSortKey(?string $interno): array { $s = trim((string) ($interno ?? '')); if ($s === '' || $s === '—') { return ['~', PHP_INT_MAX, '']; } $u = strtoupper($s); $m = null; if (preg_match('/^([A-Z]+)\s*[-\/]\s*(\d+)(.*)$/', $u, $m)) { return [$m[1], (int) $m[2], trim((string) ($m[3] ?? ''))]; } if (preg_match('/^(\d+)(.*)$/', $u, $m)) { return ['', (int) $m[1], trim((string) ($m[2] ?? ''))]; } return [$u, PHP_INT_MAX, $u]; } protected function pickUnitaId(?int $candidate): ?int { if (empty($this->unitaOptionsCache)) { $this->refreshUnitaOptions(); } if ($candidate && array_key_exists($candidate, $this->unitaOptionsCache)) { return $candidate; } $first = array_key_first($this->unitaOptionsCache); return is_int($first) ? $first : null; } protected function loadUnita(): void { $this->unita = null; $this->millesimiPerTabella = []; $this->dirittiProprieta = []; $this->relazioniPerTipo = []; $this->nominativiStorici = []; $this->recapitiServizio = []; $this->ripartizioniPerTabella = []; $this->preventiviPerTabella = []; $this->totaliPerGestione = []; $this->rateEmessePerCategoria = []; if (! $this->unitaId) { $this->tab = 'riepilogo'; return; } $this->unita = UnitaImmobiliare::withTrashed() ->with([ 'palazzinaObj', 'soggetti', 'stabile', 'dettagliMillesimi.tabellaMillesimale', 'relazioniPersoneAttive.persona.emailMultiple', 'unitaRecapitiServizio.persona', ]) ->where('stabile_id', $this->stabileId) ->whereKey($this->unitaId) ->first(); if ($this->unita) { $this->hydrateMillesimi(); $this->hydrateDiritti(); $this->hydrateRelazioni(); $this->hydrateNominativiStorici(); $this->hydrateRecapitiServizio(); $this->hydrateRateEmesse(); $this->hydrateEstrattoCompatto(); $this->hydrateConguagliIniziali(); $this->hydrateRipartizioni(); $this->hydratePreventivi(); } $this->tab = 'riepilogo'; } protected function buildUnitaIndexQuery() { $query = UnitaImmobiliare::query() ->where('stabile_id', $this->stabileId); return match ($this->unitaVisibilita) { 'tutte' => $query->withTrashed(), 'archiviate' => $query->onlyTrashed(), default => $query, }; } protected function normalizeUnitaVisibilita(mixed $value): string { $candidate = strtolower(trim((string) $value)); return in_array($candidate, ['attive', 'tutte', 'archiviate'], true) ? $candidate : 'attive'; } protected function getEditUnitaFormData(): array { if (! $this->unita) { return []; } return [ 'codice_unita' => (string) ($this->unita->codice_unita ?? ''), 'denominazione' => (string) ($this->unita->denominazione ?? ''), 'tipo_unita' => (string) ($this->unita->tipo_unita ?? ''), 'categoria_catastale' => (string) ($this->unita->categoria_catastale ?? ''), 'scala' => (string) ($this->unita->scala ?? ''), 'piano' => (string) ($this->unita->piano ?? ''), 'interno' => (string) ($this->unita->interno ?? ''), 'stato_occupazione' => (string) ($this->unita->stato_occupazione ?? ''), 'utilizzo_attuale' => (string) ($this->unita->utilizzo_attuale ?? ''), 'attiva' => (bool) ($this->unita->attiva ?? true), 'note' => (string) ($this->unita->note ?? ''), ]; } protected function saveEditedUnita(array $data): void { if (! $this->unita) { return; } $this->unita->fill([ 'codice_unita' => $this->normalizeOptionalString($data['codice_unita'] ?? null), 'denominazione' => $this->normalizeOptionalString($data['denominazione'] ?? null), 'tipo_unita' => $this->normalizeOptionalString($data['tipo_unita'] ?? null), 'categoria_catastale' => $this->normalizeOptionalString($data['categoria_catastale'] ?? null), 'scala' => $this->normalizeOptionalString($data['scala'] ?? null), 'piano' => $this->normalizeOptionalString($data['piano'] ?? null), 'interno' => $this->normalizeOptionalString($data['interno'] ?? null), 'stato_occupazione' => $this->normalizeOptionalString($data['stato_occupazione'] ?? null), 'utilizzo_attuale' => $this->normalizeOptionalString($data['utilizzo_attuale'] ?? null), 'attiva' => (bool) ($data['attiva'] ?? false), 'note' => $this->normalizeOptionalString($data['note'] ?? null), ]); $this->unita->updated_by = Auth::id(); $this->unita->save(); $this->refreshUnitaOptions(); $this->loadUnita(); Notification::make() ->title('Unità aggiornata') ->success() ->send(); } public function hydrateNominativiStorici(): void { $this->nominativiStorici = []; if (! $this->unita) { return; } if (! DbSchema::hasTable('unita_immobiliare_nominativi')) { return; } $rows = $this->unita->nominativiStorici() ->orderBy('data_inizio') ->orderByDesc('ruolo') ->orderByDesc('id') ->get(); if ($rows->isEmpty()) { return; } $this->nominativiStorici = $rows->map(function ($row): array { $rawInizio = method_exists($row, 'getRawOriginal') ? $row->getRawOriginal('data_inizio') : null; $rawFine = method_exists($row, 'getRawOriginal') ? $row->getRawOriginal('data_fine') : null; $payload = is_array($row->legacy_payload) ? $row->legacy_payload : []; $inizioVal = $this->normalizeLegacyDateValue($rawInizio ?? $row->data_inizio ?? null); $fineVal = $this->normalizeLegacyDateValue($rawFine ?? $row->data_fine ?? null); $inizio = $inizioVal ? Carbon::parse($inizioVal)->format('d/m/Y') : null; $fine = $fineVal ? Carbon::parse($fineVal)->format('d/m/Y') : null; $periodo = null; if ($inizio && $fine) { $periodo = 'Da ' . $inizio . ' · fino a ' . $fine; } elseif ($inizio) { $periodo = 'Da ' . $inizio; } elseif ($fine) { $periodo = 'Fino al ' . $fine; } else { $periodo = 'Da sempre'; } $percentuale = null; if (is_numeric($row->percentuale)) { $percentuale = number_format((float) $row->percentuale, 3, ',', '.') . '%'; } $details = []; $dirittoLabel = trim((string) ($payload['diritto_label'] ?? '')); if ($dirittoLabel !== '') { $details[] = 'Diritto: ' . $dirittoLabel; } $legacyYears = $payload['legacy_years'] ?? []; if (is_array($legacyYears) && $legacyYears !== []) { $details[] = 'Annualita: ' . implode(', ', array_map('strval', $legacyYears)); } $sourceLabel = match ((string) ($row->fonte ?? '')) { 'legacy_comproprietari' => 'Legacy comproprietari', 'legacy_condomin' => 'Legacy condomin', default => $row->fonte ?: null, }; return [ 'ruolo' => $this->formatNominativoStoricoRuolo((string) ($row->ruolo ?? ''), (string) ($row->fonte ?? ''), $payload), 'nominativo' => $row->nominativo ?: '—', 'periodo' => $periodo, 'percentuale' => $percentuale, 'fonte' => $sourceLabel, 'detail' => $details === [] ? null : implode(' · ', $details), ]; })->all(); } public function hydrateRecapitiServizio(): void { $this->recapitiServizio = []; if (! $this->unita) { return; } $resolver = app(RecapitiServizioResolver::class); foreach (UnitaRecapitoServizio::serviceLabels() as $serviceType => $label) { $rows = $resolver->resolveForUnita($this->unita, $serviceType); $this->recapitiServizio[$serviceType] = [ 'label' => $label, 'rows' => array_map(function (array $row): array { $source = (string) ($row['source'] ?? 'persona'); return [ 'email' => (string) ($row['email'] ?? ''), 'persona' => (string) ($row['persona'] ?? ''), 'label' => (string) ($row['label'] ?? ''), 'source' => $source, 'sourceLabel' => $source === 'override_unita' ? 'Override unita' : 'Anagrafica persona', ]; }, $rows), ]; } } private function formatNominativoStoricoRuolo(string $ruolo, string $fonte, array $payload): string { if ($fonte === 'legacy_comproprietari') { $dirittoLabel = trim((string) ($payload['diritto_label'] ?? '')); return $dirittoLabel !== '' ? $dirittoLabel : 'Comproprietario'; } return match (strtoupper($ruolo)) { 'I' => 'Inquilino', 'C' => 'Condomino', default => $ruolo !== '' ? $ruolo : '—', }; } private function extractPersonaEmails(Persona $persona): array { $emails = []; $primary = mb_strtolower(trim((string) ($persona->email_principale ?? ''))); if ($primary !== '') { $emails[] = $primary; } foreach ($persona->emailMultiple as $row) { if (! $row->attiva) { continue; } $email = mb_strtolower(trim((string) ($row->email ?? ''))); if ($email !== '') { $emails[] = $email; } } return array_values(array_unique($emails)); } private function normalizeOptionalString(mixed $value): ?string { $string = trim((string) ($value ?? '')); return $string !== '' ? $string : null; } protected function hydrateRateEmesse(): void { $this->rateEmessePerCategoria = [ 'condomini' => [], 'inquilini' => [], ]; if (! DbSchema::hasTable('rate_emesse')) { return; } if (! $this->unita) { return; } $ownerIds = Proprieta::query() ->where('unita_immobiliare_id', $this->unita->id) ->pluck('soggetto_id') ->filter(fn($v) => is_numeric($v) && (int) $v > 0) ->map(fn($v) => (int) $v) ->unique() ->values() ->all(); $ownerLookup = array_fill_keys($ownerIds, true); $rowsQuery = RataEmessaNg::query() ->where('unita_immobiliare_id', $this->unita->id) ->with([ 'soggettoResponsabile:id,nome,cognome,ragione_sociale,codice_fiscale', 'pianoRateizzazione:id,stabile_id,descrizione', ]) ->orderByDesc('data_emissione') ->orderByDesc('id') ->limit(5000); $rows = $rowsQuery->get(); if ($rows->isEmpty()) { return; } $groups = $rows ->groupBy('soggetto_responsabile_id') ->map(function ($items, $soggettoId) use ($ownerLookup): array { $first = $items->first(); $s = $first?->soggettoResponsabile; $nome = '—'; if ($s) { $nome = trim((string) ($s->ragione_sociale ?? '')); if ($nome === '') { $nome = trim((string) ($s->nome ?? '') . ' ' . (string) ($s->cognome ?? '')); } if ($nome === '') { $nome = 'Soggetto #' . (int) $s->id; } } $totAddebitato = (float) $items->sum(fn(RataEmessaNg $r) => (float) $r->importo_addebitato_soggetto); $totPagato = (float) $items->sum(fn(RataEmessaNg $r) => (float) $r->importo_pagato); $residuo = $totAddebitato - $totPagato; $scadute = $items->filter(function (RataEmessaNg $r): bool { try { return strtoupper((string) $r->stato_rata) === 'EMESSA' && $r->data_scadenza && $r->data_scadenza->isPast(); } catch (\Throwable $e) { return false; } })->count(); $categoria = isset($ownerLookup[(int) $soggettoId]) ? 'condomini' : 'inquilini'; return [ 'categoria' => $categoria, 'soggetto_id' => is_numeric($soggettoId) ? (int) $soggettoId : null, 'nome' => $nome, 'codice_fiscale' => $s?->codice_fiscale, 'numero_rate' => $items->count(), 'scadute' => $scadute, 'totale_addebitato' => $totAddebitato, 'totale_pagato' => $totPagato, 'residuo' => $residuo, 'ultimo_piano' => $first?->pianoRateizzazione?->descrizione, ]; }) ->values(); foreach ($groups as $g) { $cat = $g['categoria'] ?? 'inquilini'; $this->rateEmessePerCategoria[$cat][] = $g; } foreach (['condomini', 'inquilini'] as $cat) { usort($this->rateEmessePerCategoria[$cat], function (array $a, array $b): int { return ($b['residuo'] <=> $a['residuo']) ?: (($b['totale_addebitato'] ?? 0) <=> ($a['totale_addebitato'] ?? 0)); }); } // Default tab estratto: preferisci condomini se presenti, altrimenti inquilini. if ($this->estrattoTipo !== 'condomini' && $this->estrattoTipo !== 'inquilini') { $this->estrattoTipo = 'condomini'; } if ($this->estrattoTipo === 'condomini' && empty($this->rateEmessePerCategoria['condomini'])) { if (! empty($this->rateEmessePerCategoria['inquilini'])) { $this->estrattoTipo = 'inquilini'; } } } protected function hydrateEstrattoCompatto(): void { $this->estrattoCompattoRateRows = []; $this->estrattoCompattoIncassi = []; if (! DbSchema::hasTable('rate_emesse')) { return; } if (! $this->unita) { return; } $rateRowsQuery = RataEmessaNg::query() ->where('unita_immobiliare_id', $this->unita->id) ->orderBy('data_emissione') ->orderBy('id') ->limit(5000); $rateRows = $rateRowsQuery->get(); if ($rateRows->isNotEmpty()) { $this->estrattoCompattoRateRows = $rateRows->map(function (RataEmessaNg $r): array { $note = (string) ($r->note ?? ''); $tipo = null; if ($note !== '' && preg_match('/\btipo_soggetto=([CI])\b/', $note, $m)) { $tipo = (string) ($m[1] ?? null); } $dovuto = (float) ($r->importo_addebitato_soggetto ?? 0); $pagato = (float) ($r->importo_pagato ?? 0); $residuo = $dovuto - $pagato; return [ 'id' => (int) $r->id, 'data_emissione' => $r->data_emissione?->format('d/m/Y'), 'data_scadenza' => $r->data_scadenza?->format('d/m/Y'), 'data_pagamento' => $r->data_ultimo_pagamento?->format('d/m/Y'), 'descrizione' => (string) ($r->descrizione ?? ''), 'avviso' => (int) ($r->numero_rata_progressivo ?? 0), 'tipo' => $tipo, 'dovuto' => $dovuto, 'pagato' => $pagato, 'residuo' => $residuo, ]; })->all(); } $this->estrattoCompattoIncassi = $this->loadIncassiForUnita($rateRows); } protected function hydrateConguagliIniziali(): void { $this->estrattoConguagliIniziali = []; if (! $this->unita) { return; } $legacyCondId = is_numeric($this->unita->legacy_cond_id ?? null) ? (int) $this->unita->legacy_cond_id : null; if (! $legacyCondId) { return; } if (! DbSchema::connection('gescon_import')->hasTable('dett_tab')) { return; } $codStabile = trim((string) ($this->unita->stabile?->codice_stabile ?? '')); if ($codStabile === '') { return; } $rows = DB::connection('gescon_import') ->table('dett_tab') ->where('cod_stabile', $codStabile) ->where('id_cond', (string) $legacyCondId) ->where('cod_tab', 'like', 'CONG.%') ->select([ 'legacy_year', 'cod_tab', 'n_stra', DB::raw('SUM(cons_euro) as totale'), ]) ->groupBy('legacy_year', 'cod_tab', 'n_stra') ->orderBy('legacy_year') ->orderBy('cod_tab') ->get(); if ($rows->isEmpty()) { return; } foreach ($rows as $r) { $importo = is_numeric($r->totale ?? null) ? (float) $r->totale : 0.0; if (abs($importo) < 0.00001) { continue; } $legacyYear = trim((string) ($r->legacy_year ?? '')); $label = $this->resolveLegacyYearLabel($codStabile, $legacyYear); $year = $this->extractYearFromLabel($label); $data = $year ? ('31/12/' . $year) : null; $codTab = trim((string) ($r->cod_tab ?? '')); $tipo = 'Straordinaria'; if (str_starts_with($codTab, 'CONG.O')) { $tipo = 'Ordinaria'; } elseif (str_starts_with($codTab, 'CONG.R')) { $tipo = 'Riscaldamento'; } $nStra = $r->n_stra !== null ? (int) $r->n_stra : null; if ($tipo === 'Straordinaria' && $nStra) { $tipo .= ' #' . $nStra; } $descr = 'Conguaglio iniziale ' . $tipo; $this->estrattoConguagliIniziali[] = [ 'legacy_year' => $legacyYear, 'gestione_label' => $label, 'data' => $data, 'cod_tab' => $codTab, 'tipo' => $tipo, 'importo' => $importo, 'descrizione' => $descr, ]; } } private function resolveLegacyYearLabel(string $codStabileLegacy, ?string $legacyYear): string { $legacyYear = trim((string) ($legacyYear ?? '')); if ($legacyYear === '') { return ''; } if (! DbSchema::connection('gescon_import')->hasTable('gestioni_annuali')) { return $legacyYear; } $row = DB::connection('gescon_import') ->table('gestioni_annuali') ->where('cod_stabile', $codStabileLegacy) ->where('cartella', $legacyYear) ->first(['anno_ordinario', 'anno_riscaldamento']); $label = trim((string) ($row?->anno_ordinario ?? '')); if ($label === '') { $label = trim((string) ($row?->anno_riscaldamento ?? '')); } return $label !== '' ? $label : $legacyYear; } private function extractYearFromLabel(string $label): ?string { if (preg_match('/(\d{4})/', $label, $m)) { return $m[1]; } return null; } private function loadIncassiForUnita($rateItems): array { if (! DbSchema::hasTable('incassi')) { return []; } $codCol = DbSchema::hasColumn('incassi', 'cod_cond_gescon') ? 'cod_cond_gescon' : (DbSchema::hasColumn('incassi', 'cod_cond') ? 'cod_cond' : null); $tipoCol = DbSchema::hasColumn('incassi', 'cond_inquil') ? 'cond_inquil' : (DbSchema::hasColumn('incassi', 'cond_inq') ? 'cond_inq' : null); if (! $codCol || ! $tipoCol) { return []; } $pairs = []; foreach ($rateItems as $r) { try { $note = (string) ($r->note ?? ''); if ($note === '') { continue; } $tipo = ''; $cod = ''; if (preg_match('/\btipo_soggetto=([CI])\b/', $note, $mTipo)) { $tipo = (string) ($mTipo[1] ?? ''); } elseif (preg_match('/\bcond_inq=([CI])\b/', $note, $mTipoAlt)) { $tipo = (string) ($mTipoAlt[1] ?? ''); } if (preg_match('/\bcod_cond_dest=([^\s|]+)/', $note, $mCod)) { $cod = trim((string) ($mCod[1] ?? '')); } elseif (preg_match('/\bcod_cond_src=([^\s|]+)/', $note, $mCodAlt)) { $cod = trim((string) ($mCodAlt[1] ?? '')); } if ($tipo === '' || $cod === '') { continue; } $pairs[$tipo . '|' . $cod] = ['tipo' => $tipo, 'cod' => $cod]; } catch (\Throwable $e) { continue; } } if (empty($pairs)) { return []; } $incassiQuery = Incasso::query(); if (DbSchema::hasColumn('incassi', 'condominio_id')) { $incassiQuery->where('condominio_id', $this->stabileId); } $incassiQuery->where(function ($q) use ($pairs, $codCol, $tipoCol): void { foreach ($pairs as $p) { $q->orWhere(function ($qq) use ($p, $codCol, $tipoCol): void { $qq->where($codCol, (string) $p['cod']) ->where($tipoCol, (string) $p['tipo']); }); } }); $user = Auth::user(); $annoGestione = $user instanceof User ? AnnoGestioneContext::resolveActiveAnno($user) : null; if ($annoGestione) { $annoCol = DbSchema::hasColumn('incassi', 'anno_rif') ? 'anno_rif' : (DbSchema::hasColumn('incassi', 'anno') ? 'anno' : null); $annoShort = (int) substr((string) $annoGestione, -2); $incassiQuery->where(function ($q) use ($annoCol, $annoGestione, $annoShort): void { if ($annoCol) { $q->where($annoCol, (string) $annoGestione) ->orWhere($annoCol, (string) $annoShort) ->orWhere($annoCol, str_pad((string) $annoShort, 2, '0', STR_PAD_LEFT)); } if (DbSchema::hasColumn('incassi', 'dt_empag')) { $q->orWhereYear('dt_empag', (int) $annoGestione); } if (DbSchema::hasColumn('incassi', 'data_pagamento')) { $q->orWhereYear('data_pagamento', (int) $annoGestione); } }); } $orderCol = DbSchema::hasColumn('incassi', 'dt_empag') ? 'dt_empag' : (DbSchema::hasColumn('incassi', 'data_pagamento') ? 'data_pagamento' : 'id'); $incassi = $incassiQuery ->orderByDesc($orderCol) ->orderByDesc('id') ->limit(2000) ->get(); return $incassi->map(function (Incasso $i) use ($codCol, $tipoCol): array { $importoRaw = $i->importo_pagato_euro ?? $i->importo_pagato ?? $i->importo_euro ?? $i->importo ?? 0; $importo = $this->normalizeLegacyIncassoImporto(is_numeric($importoRaw) ? (float) $importoRaw : 0.0); $dt = null; if (! empty($i->dt_empag)) { try { $dt = Carbon::parse($i->dt_empag)->format('d/m/Y'); } catch (\Throwable $e) { $dt = (string) $i->dt_empag; } } elseif (! empty($i->data_pagamento)) { try { $dt = Carbon::parse($i->data_pagamento)->format('d/m/Y'); } catch (\Throwable $e) { $dt = (string) $i->data_pagamento; } } return [ 'id' => (int) $i->id, 'data' => $dt, 'tipo' => (string) (data_get($i, $tipoCol) ?? ''), 'cod_cond' => (string) (data_get($i, $codCol) ?? ''), 'importo' => $importo, 'descrizione' => (string) ($i->descrizione ?? ''), 'n_ricevuta' => $i->n_ricevuta ?? $i->n_riferimento ?? null, ]; })->all(); } private function normalizeLegacyIncassoImporto(float $val): float { $abs = abs($val); if ($abs >= 1000000) { return $val / 10000; } if ($abs >= 100000 && fmod($abs, 1.0) === 0.0) { return $val / 100; } if ($abs >= 1000 && fmod($abs, 1.0) === 0.0 && ((int) $abs % 100) === 0) { return $val / 100; } return $val; } protected function hydrateMillesimi(): void { $this->millesimiPerTabella = $this->unita->dettagliMillesimi ->sortBy(function ($d) { $tabella = $d->tabellaMillesimale; $tipo = (string) ($tabella?->tipo_tabella ?? ($tabella?->tipo ?? '')); $ord = (int) ($d->nord ?? $tabella?->nord ?? $tabella?->ordinamento ?? 999999); return sprintf('%s|%09d', $tipo, $ord); }) ->map(function ($d) { $tabella = $d->tabellaMillesimale; $tipo = (string) ($tabella?->tipo_tabella ?? ($tabella?->tipo ?? '')); if ($tipo === '') { $tipo = 'altro'; } return [ 'tipo' => $tipo, 'gruppo' => $this->mapTipoToGruppo($tipo), 'id' => $tabella?->id, 'codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', 'nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', 'millesimi' => (float) ($d->millesimi ?? 0), ]; }) ->values() ->all(); } public function setMillesimiGruppo(string $gruppo): void { $allowed = ['ordinarie', 'riscaldamento', 'straordinarie']; if (! in_array($gruppo, $allowed, true)) { return; } $this->millesimiGruppo = $gruppo; } protected function mapTipoToGruppo(string $tipo): string { $t = strtolower(trim($tipo)); if ($t === 'r' || str_contains($t, 'risc')) { return 'riscaldamento'; } if ($t === 's' || str_contains($t, 'stra')) { return 'straordinarie'; } return 'ordinarie'; } protected function hydrateDiritti(): void { $formatDate = static function ($value) { if (empty($value)) { return null; } try { return Carbon::parse($value)->format('d/m/Y'); } catch (\Throwable $e) { return $value; } }; $this->dirittiProprieta = Proprieta::with('soggetto') ->where('unita_immobiliare_id', $this->unita->id) ->orderByDesc('percentuale_possesso') ->get() ->map(function (Proprieta $diritto) use ($formatDate) { $soggetto = $diritto->soggetto; $nome = $soggetto?->ragione_sociale ?: trim(($soggetto?->cognome ?? '') . ' ' . ($soggetto?->nome ?? '')); if ($nome === '') { $nome = $soggetto?->nome ?? ($soggetto ? 'Soggetto #' . $soggetto->id : 'Soggetto non definito'); } $percentuale = is_numeric($diritto->percentuale_possesso) ? (float) $diritto->percentuale_possesso : null; $codiceFiscale = $soggetto?->codice_fiscale; if (str_starts_with((string) $nome, 'Soggetto #') && empty($codiceFiscale) && $percentuale === null) { return null; } return [ 'id' => $diritto->id, 'soggetto_id' => $soggetto?->id, 'nome' => $nome, 'codice_fiscale' => $codiceFiscale, 'tipo' => $diritto->tipo_diritto, 'percentuale' => $percentuale, 'percentuale_label' => $percentuale !== null ? number_format($percentuale, 2, ',', '.') : null, 'data_inizio' => $formatDate($diritto->data_inizio), 'data_fine' => $formatDate($diritto->data_fine), ]; }) ->filter() ->values() ->all(); } protected function hydrateRelazioni(): void { $formatDate = static function ($value) { if (empty($value)) { return null; } try { return Carbon::parse($value)->format('d/m/Y'); } catch (\Throwable $e) { return $value; } }; $labelsRuoli = [ 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'fornitore' => 'Fornitore', 'altro' => 'Altro', ]; $labelsRuoli = [ 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'fornitore' => 'Fornitore', 'altro' => 'Altro', ]; $tipoDirittoLabels = [ 'piena_proprieta' => 'Condomino (piena_proprietà)', 'proprieta' => 'Condomino (proprietà)', 'comproprieta' => 'Condomino (comproprietà)', 'nuda_proprieta' => 'Condomino (nuda proprietà)', 'usufrutto' => 'Condomino (usufrutto)', 'usufruttuario' => 'Condomino (usufrutto)', ]; $legacyRow = $this->getLegacyCondominRow(); $legacyOwnerStart = $legacyRow?->subentrato_dal ?? null; $legacyOwnerEnd = $legacyRow?->attivo_fino_al ?? null; $legacyInqStart = $legacyRow?->inquil_dal ?? null; $legacyInqEnd = $legacyRow?->inquil_al ?? null; $legacyOwnerName = trim((string) ($legacyRow?->nom_cond ?? '')); $legacyInqName = trim((string) ($legacyRow?->inquil_nome ?? '')); $proprietari = collect($this->dirittiProprieta) ->filter(fn(array $d) => ! empty($d['soggetto_id'])) ->map(function (array $d) use ($tipoDirittoLabels): array { $tipoRaw = strtolower(trim((string) ($d['tipo'] ?? 'condomino'))); $tipoLabel = $tipoDirittoLabels[$tipoRaw] ?? 'Condomino'; $quota = $d['percentuale']; $quota = is_numeric($quota) ? (float) $quota : null; return [ 'id' => (int) ($d['id'] ?? 0), 'soggetto_id' => (int) ($d['soggetto_id'] ?? 0), 'nome' => (string) ($d['nome'] ?? '—'), 'codice_fiscale' => $d['codice_fiscale'] ?? null, 'tipo_raw' => 'condomino', 'tipo' => $tipoLabel, 'ruolo_rate' => 'C', 'quota' => $quota, 'quota_label' => $quota !== null ? number_format($quota, 2, ',', '.') : null, 'data_inizio' => $d['data_inizio'] ?? null, 'data_fine' => $d['data_fine'] ?? null, 'attivo' => true, ]; }) ->unique(fn(array $d) => (int) ($d['soggetto_id'] ?? 0)) ->values(); $today = now()->toDateString(); $relazioniMapped = RubricaRuolo::query() ->with(['contatto']) ->where('stabile_id', $this->stabileId) ->where('unita_immobiliare_id', $this->unita->id) ->where('is_attivo', true) ->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($today) { $q->whereNull('data_inizio')->orWhere('data_inizio', '<=', $today); }) ->where(function (\Illuminate\Database\Eloquent\Builder $q) use ($today) { $q->whereNull('data_fine')->orWhere('data_fine', '>=', $today); }) ->orderByDesc('is_preferito') ->orderByDesc('data_inizio') ->orderByDesc('id') ->limit(200) ->get() ->filter(fn(RubricaRuolo $relazione) => (bool) $relazione->contatto) ->map(function (RubricaRuolo $relazione) use ($formatDate, $labelsRuoli) { $contatto = $relazione->contatto; $nome = trim((string) ($contatto?->nome_completo ?? '')); if ($nome === '') { $nome = $contatto ? 'Contatto #' . $contatto->id : 'Contatto non definito'; } $tipoRaw = strtolower(trim((string) ($relazione->ruolo_standard ?? ''))); $customRaw = trim((string) ($relazione->ruolo_custom ?? '')); $hasMeaningfulCustomRole = $customRaw !== '' && ! in_array(strtolower($customRaw), ['p', 'i', 'c', 'u'], true); $tipoLabel = $labelsRuoli[$tipoRaw] ?? ($tipoRaw !== '' ? $tipoRaw : 'Altro'); if ($hasMeaningfulCustomRole) { $tipoLabel .= ' (' . $customRaw . ')'; } $quota = data_get($relazione->meta, 'right_percent'); if ($quota === null) { $quota = data_get($relazione->meta, 'percentuale_possesso'); } if (is_string($quota)) { $quota = str_replace(['%', ' '], ['', ''], $quota); $quota = str_replace(',', '.', $quota); } $quota = is_numeric($quota) ? (float) $quota : null; $ruoloRate = null; if ($tipoRaw === 'inquilino') { $ruoloRate = 'I'; } elseif ($tipoRaw === 'condomino') { $ruoloRate = 'C'; } return [ 'id' => (int) $relazione->id, 'persona_id' => (int) ($relazione->rubrica_id ?? 0), 'rubrica_id' => (int) ($relazione->rubrica_id ?? 0), 'nome' => $nome, 'codice_fiscale' => $contatto?->codice_fiscale, 'tipo_raw' => $tipoRaw, 'tipo' => $tipoLabel, 'ruolo_custom' => $hasMeaningfulCustomRole ? $customRaw : null, 'has_custom_role' => $hasMeaningfulCustomRole, 'ruolo_rate' => $ruoloRate, 'quota' => $quota, 'quota_label' => $quota !== null ? number_format($quota, 2, ',', '.') : null, 'data_inizio' => $formatDate($relazione->data_inizio), 'data_fine' => $formatDate($relazione->data_fine), 'attivo' => true, 'telefono' => trim((string) ($contatto?->telefono_cellulare ?: $contatto?->telefono_ufficio ?: $contatto?->telefono_casa ?: '')), 'email' => trim((string) ($contatto?->email ?? '')), 'rubrica_url' => $contatto ? RubricaUniversaleScheda::getUrl(['record' => (int) $contatto->id], panel : 'admin-filament'): null, 'riceve_comunicazioni' => false, 'riceve_convocazioni' => false, 'vota_assemblea' => false, ]; }); $inquilini = $relazioniMapped ->filter(function ($rel) { return $rel['attivo'] && in_array(strtolower($rel['tipo_raw'] ?? ''), [ 'inquilino', 'locatario', 'conduttore', ], true); }) ->unique(fn(array $r) => (int) ($r['persona_id'] ?? 0)) ->values(); if ($inquilini->isNotEmpty()) { $inquiliniIds = $inquilini ->pluck('persona_id') ->filter(fn($id) => is_numeric($id) && (int) $id > 0) ->map(fn($id) => (int) $id) ->values() ->all(); $inquiliniCf = $inquilini ->pluck('codice_fiscale') ->filter() ->map(fn($cf) => strtoupper(trim((string) $cf))) ->filter(fn($cf) => $cf !== '') ->values() ->all(); $proprietari = $proprietari->reject(function (array $r) use ($inquiliniIds, $inquiliniCf): bool { $id = is_numeric($r['soggetto_id'] ?? null) ? (int) $r['soggetto_id'] : 0; if ($id > 0 && in_array($id, $inquiliniIds, true)) { return true; } $cf = strtoupper(trim((string) ($r['codice_fiscale'] ?? ''))); return $cf !== '' && in_array($cf, $inquiliniCf, true); })->values(); } $altri = $relazioniMapped ->filter(function ($rel) { return $rel['attivo'] && ( ! in_array(strtolower($rel['tipo_raw'] ?? ''), [ 'condomino', 'proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'usufrutto', 'inquilino', 'locatario', 'conduttore', ], true) || ! empty($rel['has_custom_role']) ); }) ->unique(fn(array $r) => strtolower((string) ($r['tipo_raw'] ?? 'altro')) . '|' . strtolower((string) ($r['ruolo_custom'] ?? '')) . '|' . (int) ($r['persona_id'] ?? 0)) ->values(); if ($proprietari->isEmpty()) { $proprietari = $relazioniMapped ->filter(function ($rel) { return $rel['attivo'] && in_array(strtolower($rel['tipo_raw'] ?? ''), [ 'condomino', 'proprietario', 'comproprietario', 'nudo_proprietario', 'usufruttuario', 'usufrutto', ], true); }) ->unique(fn(array $r) => (int) ($r['persona_id'] ?? 0)) ->values(); } $legacyOwnerStartC = $this->parseLegacyDateToCarbon($legacyOwnerStart); $legacyOwnerEndC = $this->parseLegacyDateToCarbon($legacyOwnerEnd); $legacyOwnerNewStartC = $legacyOwnerEndC ? $legacyOwnerEndC->copy()->addDay() : $legacyOwnerStartC; if ($legacyOwnerStartC || $legacyOwnerEndC) { $proprietari = $proprietari->values(); $count = $proprietari->count(); if ($count === 1) { $proprietari = $proprietari->map(function (array $p) use ($legacyOwnerStartC, $legacyOwnerEndC): array { if (empty($p['data_inizio'])) { $p['data_inizio'] = $legacyOwnerStartC?->format('d/m/Y') ?? null; } if (empty($p['data_fine'])) { $p['data_fine'] = $legacyOwnerEndC?->format('d/m/Y') ?? null; } return $p; }); } elseif ($count >= 2) { $idxNew = 0; if ($legacyOwnerName !== '') { foreach ($proprietari as $idx => $p) { $nome = trim((string) ($p['nome'] ?? '')); if ($nome !== '' && str_contains(mb_strtolower($nome), mb_strtolower($legacyOwnerName))) { $idxNew = $idx; break; } } } $idxOld = $idxNew === 0 ? ($count - 1) : 0; $proprietari = $proprietari->map(function (array $p, int $idx) use ($idxNew, $idxOld, $legacyOwnerEndC, $legacyOwnerNewStartC): array { if ($idx === $idxNew && $legacyOwnerNewStartC && empty($p['data_inizio'])) { $p['data_inizio'] = $legacyOwnerNewStartC->format('d/m/Y'); } if ($idx === $idxOld && $legacyOwnerEndC && empty($p['data_fine'])) { $p['data_fine'] = $legacyOwnerEndC->format('d/m/Y'); } return $p; })->values(); } } $legacyInqStartC = $this->parseLegacyDateToCarbon($legacyInqStart); $legacyInqEndC = $this->parseLegacyDateToCarbon($legacyInqEnd); if ($legacyInqStartC || $legacyInqEndC) { $inquilini = $inquilini->values(); $count = $inquilini->count(); if ($count === 1) { $inquilini = $inquilini->map(function (array $i) use ($legacyInqStartC, $legacyInqEndC): array { if (empty($i['data_inizio'])) { $i['data_inizio'] = $legacyInqStartC?->format('d/m/Y') ?? null; } if (empty($i['data_fine'])) { $i['data_fine'] = $legacyInqEndC?->format('d/m/Y') ?? null; } return $i; }); } elseif ($count >= 2) { $idxNew = 0; if ($legacyInqName !== '') { foreach ($inquilini as $idx => $i) { $nome = trim((string) ($i['nome'] ?? '')); if ($nome !== '' && str_contains(mb_strtolower($nome), mb_strtolower($legacyInqName))) { $idxNew = $idx; break; } } } $idxOld = $idxNew === 0 ? ($count - 1) : 0; $inquilini = $inquilini->map(function (array $i, int $idx) use ($idxNew, $idxOld, $legacyInqEndC, $legacyInqStartC): array { if ($idx === $idxNew && $legacyInqStartC && empty($i['data_inizio'])) { $i['data_inizio'] = $legacyInqStartC->format('d/m/Y'); } if ($idx === $idxOld && $legacyInqEndC && empty($i['data_fine'])) { $i['data_fine'] = $legacyInqEndC->format('d/m/Y'); } return $i; })->values(); } } $this->relazioniPerTipo = [ 'proprietari' => $proprietari->values()->all(), 'inquilini' => $inquilini->values()->all(), 'altri' => $altri->values()->all(), ]; } private function getLegacyCondominRow(): ?object { if ($this->legacyCondominRow !== null) { return $this->legacyCondominRow; } if (! DbSchema::connection('gescon_import')->hasTable('condomin')) { return $this->legacyCondominRow = null; } if (! $this->unita) { return $this->legacyCondominRow = null; } $codStabile = trim((string) ($this->unita->stabile?->codice_stabile ?? '')); if ($codStabile === '') { return $this->legacyCondominRow = null; } $scala = trim((string) ($this->unita->scala ?? '')); $interno = trim((string) ($this->unita->interno ?? '')); if ($interno === '') { return $this->legacyCondominRow = null; } $query = DB::connection('gescon_import') ->table('condomin') ->where('cod_stabile', $codStabile) ->where('interno', $interno); if ($scala !== '') { $query->where('scala', $scala); } $row = $query ->orderByDesc('legacy_year') ->orderByDesc('id') ->first([ 'subentrato_dal', 'attivo_fino_al', 'inquil_dal', 'inquil_al', 'nom_cond', 'inquil_nome', ]); if (! $row) { return $this->legacyCondominRow = null; } $row->subentrato_dal = $this->normalizeLegacyDateValue($row->subentrato_dal ?? null); $row->attivo_fino_al = $this->normalizeLegacyDateValue($row->attivo_fino_al ?? null); $row->inquil_dal = $this->normalizeLegacyDateValue($row->inquil_dal ?? null); $row->inquil_al = $this->normalizeLegacyDateValue($row->inquil_al ?? null); return $this->legacyCondominRow = $row; } private function normalizeLegacyDateValue(mixed $value): mixed { if (empty($value)) { return null; } if (is_string($value)) { $raw = trim($value); if ($raw === '') { return null; } if (preg_match('/^\d{2}\/\d{2}\/\d{2}$/', $raw)) { try { return Carbon::createFromFormat('d/m/y', $raw)->format('Y-m-d'); } catch (\Throwable) { return $raw; } } if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $raw)) { try { return Carbon::createFromFormat('d/m/Y', $raw)->format('Y-m-d'); } catch (\Throwable) { return $raw; } } } return $value; } private function parseLegacyDateToCarbon(mixed $value): ?Carbon { $norm = $this->normalizeLegacyDateValue($value); if (empty($norm)) { return null; } try { return Carbon::parse($norm); } catch (\Throwable) { return null; } } protected function hydrateRipartizioni(): void { if (! DbSchema::hasTable('dettaglio_ripartizione_spese')) { $this->ripartizioniPerTabella = $this->fallbackVociSpesa(); return; } $details = DettaglioRipartizioneSpese::with([ 'ripartizioneSpese.voceSpesa', 'ripartizioneSpese.tabellaMillesimale', ]) ->where('unita_immobiliare_id', $this->unita->id) ->orderByDesc('created_at') ->limit(100) ->get(); $grouped = $details->map(function (DettaglioRipartizioneSpese $d) { $rip = $d->ripartizioneSpese; $tabella = $rip?->tabellaMillesimale; $voce = $rip?->voceSpesa; return [ 'tabella_id' => $tabella?->id, 'tabella_codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', 'tabella_nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', 'voce_codice' => $voce?->codice, 'voce_descrizione' => $voce?->descrizione, 'tipo_gestione' => $voce?->tipo_gestione, 'calcolo' => $rip?->tipo_ripartizione, 'percentuale_applicata' => (float) ($d->percentuale_applicata ?? 0), 'percentuale_inquilino' => (float) ($d->percentuale_inquilino ?? 0), 'quota_finale' => (float) ($d->quota_finale ?? 0), 'importo_proprietario' => (float) ($d->importo_proprietario ?? 0), 'importo_inquilino' => (float) ($d->importo_inquilino ?? 0), ]; })->groupBy('tabella_id'); $this->ripartizioniPerTabella = $grouped->map(function ($items) { return [ 'tabella_codice' => $items->first()['tabella_codice'] ?? 'TAB', 'tabella_nome' => $items->first()['tabella_nome'] ?? 'Tabella millesimale', 'righe' => $items->values()->all(), ]; })->values()->all(); } protected function fallbackVociSpesa(): array { if (! DbSchema::hasTable('voci_spesa')) { return []; } $voci = VoceSpesa::query() ->where('stabile_id', $this->stabileId) ->with('tabellaMillesimaleDefault') ->orderBy('ordinamento') ->get(); $grouped = $voci->map(function (VoceSpesa $voce) { $tabella = $voce->tabellaMillesimaleDefault; return [ 'tabella_id' => $tabella?->id, 'tabella_codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB', 'tabella_nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella millesimale', 'voce_codice' => $voce->codice, 'voce_descrizione' => $voce->descrizione, 'tipo_gestione' => $voce->tipo_gestione, 'calcolo' => $voce->categoria, 'percentuale_applicata' => 0.0, 'percentuale_inquilino' => 0.0, 'quota_finale' => 0.0, 'importo_proprietario' => 0.0, 'importo_inquilino' => 0.0, ]; })->groupBy('tabella_id'); return $grouped->map(function ($items) { return [ 'tabella_codice' => $items->first()['tabella_codice'] ?? 'TAB', 'tabella_nome' => $items->first()['tabella_nome'] ?? 'Tabella millesimale', 'righe' => $items->values()->all(), ]; })->values()->all(); } protected function hydratePreventivi(): void { $this->preventiviPerTabella = []; $this->totaliPerGestione = []; // Preferenza: usa le voci dal dominio (pagina "Voci di spesa") così le modifiche importi/% // si riflettono direttamente nella scheda Unità immobiliare. if (DbSchema::hasTable('voci_spesa')) { $user = Auth::user(); // Se disponibile gestioni_contabili, limita alle gestioni dell'anno attivo (tutte le tipologie). $gestioneIds = []; if ($user instanceof User && DbSchema::hasTable('gestioni_contabili') && DbSchema::hasColumn('voci_spesa', 'gestione_contabile_id')) { $anno = AnnoGestioneContext::resolveActiveAnno($user); foreach (['ordinaria', 'riscaldamento', 'straordinaria'] as $tipo) { $match = GestioneContabile::query() ->where('stabile_id', $this->stabileId) ->where('anno_gestione', $anno) ->where('tipo_gestione', $tipo) ->where('stato', 'aperta') ->orderByDesc('gestione_attiva') ->orderByDesc('id') ->first(); if (! $match) { $match = GestioneContabile::query() ->where('stabile_id', $this->stabileId) ->where('tipo_gestione', $tipo) ->where('stato', 'aperta') ->orderByDesc('gestione_attiva') ->orderByDesc('anno_gestione') ->orderByDesc('id') ->first(); } if ($match?->id) { $gestioneIds[] = (int) $match->id; } } $gestioneIds = array_values(array_unique(array_filter($gestioneIds))); } $vociDomain = VoceSpesa::query() ->where('stabile_id', $this->stabileId) ->when(! empty($gestioneIds), fn($q) => $q->whereIn('gestione_contabile_id', $gestioneIds)) ->with('tabellaMillesimaleDefault') ->orderBy('ordinamento') ->orderBy('codice') ->get(); if ($vociDomain->isNotEmpty()) { $tabellaCodes = $vociDomain ->map(fn(VoceSpesa $v) => $v->tabellaMillesimaleDefault?->codice_tabella) ->filter() ->unique() ->values() ->all(); $millesimiPerCodice = $this->unita->dettagliMillesimi ->filter(function ($d) { return $d->tabellaMillesimale !== null; }) ->keyBy(function ($d) { $tabella = $d->tabellaMillesimale; return $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? null; }); // Importi reali (prev_euro) per unità e tabella, separati per ruolo C/I. $importiPerTabella = collect(); if (DbSchema::hasTable('dettaglio_importi_tabella') && ! empty($tabellaCodes)) { $baseImporti = DB::table('dettaglio_importi_tabella as dit') ->join('tabelle_millesimali as tm', 'tm.id', '=', 'dit.tabella_millesimale_id') ->where('tm.stabile_id', $this->stabileId) ->where('dit.unita_immobiliare_id', $this->unita->id) ->whereIn('tm.codice_tabella', $tabellaCodes) ->selectRaw('tm.codice_tabella as codice_tabella, dit.ruolo_legacy as ruolo_legacy, SUM(dit.prev_euro) as prev_euro') ->groupBy('tm.codice_tabella', 'dit.ruolo_legacy'); if (DbSchema::hasColumn('dettaglio_importi_tabella', 'legacy_year')) { $maxYears = DB::table('dettaglio_importi_tabella') ->selectRaw('tabella_millesimale_id, unita_immobiliare_id, ruolo_legacy, MAX(legacy_year) as legacy_year') ->where('unita_immobiliare_id', $this->unita->id) ->groupBy('tabella_millesimale_id', 'unita_immobiliare_id', 'ruolo_legacy'); $baseImporti->joinSub($maxYears, 'mx', function ($join) { $join->on('mx.tabella_millesimale_id', '=', 'dit.tabella_millesimale_id') ->on('mx.unita_immobiliare_id', '=', 'dit.unita_immobiliare_id') ->on('mx.ruolo_legacy', '=', 'dit.ruolo_legacy') ->on('mx.legacy_year', '=', 'dit.legacy_year'); }); } $importiPerTabella = $baseImporti->get()->groupBy('codice_tabella'); } $tabelleDomain = DB::table('tabelle_millesimali') ->where('stabile_id', $this->stabileId) ->when(! empty($tabellaCodes), fn($q) => $q->whereIn('codice_tabella', $tabellaCodes)) ->get(['id', 'codice_tabella', 'denominazione', 'tipo_tabella', 'tipo_calcolo', 'totale_millesimi']) ->keyBy('codice_tabella'); $totaliGestione = ['O' => 0.0, 'R' => 0.0, 'S' => 0.0]; $this->preventiviPerTabella = $vociDomain ->filter(fn(VoceSpesa $v) => (bool) $v->tabellaMillesimaleDefault?->codice_tabella) ->groupBy(fn(VoceSpesa $v) => (string) $v->tabellaMillesimaleDefault?->codice_tabella) ->map(function ($items, $tabellaCode) use ($importiPerTabella, $tabelleDomain, $millesimiPerCodice, &$totaliGestione) { $totalPreventivo = (float) $items->sum(fn(VoceSpesa $v) => (float) ($v->importo_default ?? 0)); $domainTab = $tabelleDomain->get($tabellaCode); $tabellaNome = $domainTab?->denominazione ?: (string) $tabellaCode; // Tipo gestione (O/R/S) per i box totali $tipoRaw = (string) ($items->first()?->tipo_gestione ?? 'ordinaria'); $tipoGestioneKey = match ($tipoRaw) { 'riscaldamento' => 'R', 'straordinaria' => 'S', default => 'O', }; $tabellaMillesimi = $millesimiPerCodice->get($tabellaCode); $unitMillesimi = (float) ($tabellaMillesimi->millesimi ?? 0); $tabTotalMm = (float) ($domainTab?->totale_millesimi ?? 0); $ratio = ($tabTotalMm > 0) ? ($unitMillesimi / $tabTotalMm) : 0.0; // Importi reali per tabella e unità (prev_euro) $impRows = $importiPerTabella->get($tabellaCode) ?? collect(); $impProp = (float) $impRows ->filter(fn($r) => (string) ($r->ruolo_legacy ?? '') !== 'I') ->sum(fn($r) => (float) ($r->prev_euro ?? 0)); $impInq = (float) $impRows ->filter(fn($r) => (string) ($r->ruolo_legacy ?? '') === 'I') ->sum(fn($r) => (float) ($r->prev_euro ?? 0)); $impTot = $impProp + $impInq; $hasImporti = abs($impTot) > 0.00001; $useImportiSplit = $hasImporti && abs($impInq) > 0.00001; // Quota unità tabella $quotaUnitTabella = $hasImporti ? $impTot : ($totalPreventivo * $ratio); $rows = $items->map(function (VoceSpesa $voce) use ($hasImporti, $useImportiSplit, $impInq, $quotaUnitTabella, $totalPreventivo, $ratio) { $importoVoce = (float) ($voce->importo_default ?? 0); $percInq = is_numeric($voce->percentuale_inquilino ?? null) ? (float) $voce->percentuale_inquilino : 0.0; if ($hasImporti) { $share = ($totalPreventivo != 0.0) ? ($importoVoce / $totalPreventivo) : 0.0; $quotaUnita = $quotaUnitTabella * $share; if ($useImportiSplit) { $quotaInquilinoVoce = $impInq * $share; } else { $quotaInquilinoVoce = $quotaUnita * ($percInq / 100); } } else { $quotaUnita = $importoVoce * $ratio; $quotaInquilinoVoce = $quotaUnita * ($percInq / 100); } $quotaProprietarioVoce = $quotaUnita - $quotaInquilinoVoce; $percentualeInquilinoVoce = $quotaUnita != 0.0 ? ($quotaInquilinoVoce / $quotaUnita) * 100 : 0.0; return [ 'codice' => (string) ($voce->codice ?? ''), 'descrizione' => (string) ($voce->descrizione ?? ''), 'importo_preventivato' => $importoVoce, 'quota_unita' => $quotaUnita, 'importo_proprietario' => $quotaProprietarioVoce, 'importo_inquilino' => $quotaInquilinoVoce, 'percentuale_inquilino' => $percentualeInquilinoVoce, ]; })->values()->all(); if ($useImportiSplit) { $quotaProprietario = $impProp; $quotaInquilino = $impInq; $percentualeInquilino = ($impProp + $impInq) != 0.0 ? ($impInq / ($impProp + $impInq)) * 100 : 0.0; } else { $quotaProprietario = 0.0; $quotaInquilino = 0.0; foreach ($rows as $row) { $quotaProprietario += (float) ($row['importo_proprietario'] ?? 0); $quotaInquilino += (float) ($row['importo_inquilino'] ?? 0); } $percentualeInquilino = ($quotaProprietario + $quotaInquilino) != 0.0 ? ($quotaInquilino / ($quotaProprietario + $quotaInquilino)) * 100 : 0.0; } if (array_key_exists($tipoGestioneKey, $totaliGestione)) { $totaliGestione[$tipoGestioneKey] += ($quotaProprietario + $quotaInquilino); } return [ 'tabella_codice' => $tabellaCode, 'tabella_nome' => $tabellaNome, 'tipo' => $tipoGestioneKey, 'total_preventivo' => $totalPreventivo, 'quota_unit' => $quotaUnitTabella, 'importo_proprietario' => $quotaProprietario, 'importo_inquilino' => $quotaInquilino, 'percentuale_inquilino' => $percentualeInquilino, 'tabella_millesimi' => $tabTotalMm, 'unit_millesimi' => $unitMillesimi, 'voci' => $rows, ]; }) ->values() ->all(); $this->totaliPerGestione = $totaliGestione; return; } } $importConn = 'gescon_import'; $hasVocSpe = DbSchema::connection($importConn)->hasTable('voc_spe'); $hasDetTab = DbSchema::connection($importConn)->hasTable('dett_tab'); if (! $hasVocSpe || ! $hasDetTab) { return; } $codiceStabile = $this->unita->stabile?->codice_stabile ?? $this->unita->stabile?->codice_interno ?? $this->unita->stabile?->old_id; $voci = DB::connection($importConn) ->table('voc_spe') ->when($codiceStabile && DbSchema::connection($importConn)->hasColumn('voc_spe', 'cod_stabile'), function ($q) use ($codiceStabile) { $q->where('cod_stabile', $codiceStabile); }) ->get(); if ($voci->isEmpty()) { return; } $tabellaCodes = $voci->pluck('tabella')->filter()->unique()->values()->all(); $millesimiPerCodice = $this->unita->dettagliMillesimi ->filter(function ($d) { return $d->tabellaMillesimale !== null; }) ->keyBy(function ($d) { $tabella = $d->tabellaMillesimale; return $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? null; }); // Importi reali (prev_euro) per unità e tabella, separati per ruolo C/I. // Usati per tabelle a consumo/individuali (ACQUA/RISC.L/INDIV./CONG.* ecc) quando presenti. $importiPerTabella = collect(); if (DbSchema::hasTable('dettaglio_importi_tabella')) { $baseImporti = DB::table('dettaglio_importi_tabella as dit') ->join('tabelle_millesimali as tm', 'tm.id', '=', 'dit.tabella_millesimale_id') ->where('tm.stabile_id', $this->stabileId) ->where('dit.unita_immobiliare_id', $this->unita->id) ->whereIn('tm.codice_tabella', $tabellaCodes) ->selectRaw('tm.codice_tabella as codice_tabella, dit.ruolo_legacy as ruolo_legacy, SUM(dit.prev_euro) as prev_euro') ->groupBy('tm.codice_tabella', 'dit.ruolo_legacy'); if (DbSchema::hasColumn('dettaglio_importi_tabella', 'legacy_year')) { $maxYears = DB::table('dettaglio_importi_tabella') ->selectRaw('tabella_millesimale_id, unita_immobiliare_id, ruolo_legacy, MAX(legacy_year) as legacy_year') ->where('unita_immobiliare_id', $this->unita->id) ->groupBy('tabella_millesimale_id', 'unita_immobiliare_id', 'ruolo_legacy'); $baseImporti->joinSub($maxYears, 'mx', function ($join) { $join->on('mx.tabella_millesimale_id', '=', 'dit.tabella_millesimale_id') ->on('mx.unita_immobiliare_id', '=', 'dit.unita_immobiliare_id') ->on('mx.ruolo_legacy', '=', 'dit.ruolo_legacy') ->on('mx.legacy_year', '=', 'dit.legacy_year'); }); } $importiPerTabella = $baseImporti->get()->groupBy('codice_tabella'); } // Cache info tabella millesimale nel dominio $tabelleDomain = DB::table('tabelle_millesimali') ->where('stabile_id', $this->stabileId) ->whereIn('codice_tabella', $tabellaCodes) ->get(['id', 'codice_tabella', 'denominazione', 'tipo_tabella', 'tipo_calcolo', 'totale_millesimi']) ->keyBy('codice_tabella'); $totaliGestione = ['O' => 0.0, 'R' => 0.0, 'S' => 0.0]; $this->preventiviPerTabella = collect($voci) ->groupBy('tabella') ->map(function ($items, $tabellaCode) use ($importiPerTabella, $tabelleDomain, $millesimiPerCodice, &$totaliGestione) { $totalPreventivo = (float) $items->sum(function ($v) { return (float) ($v->preventivo_euro ?? 0); }); $domainTab = $tabelleDomain->get($tabellaCode); $tabellaNome = $domainTab?->denominazione ?: ($items->first()->tabella_descrizione ?? $items->first()->descrizione ?? $tabellaCode); $tipoGestione = $items->first()->v_ors ?? $items->first()->tipo_gestione ?? $items->first()->tipo ?? 'O'; $tipoGestioneKey = strtoupper(substr((string) $tipoGestione, 0, 1)); $tabellaMillesimi = $millesimiPerCodice->get($tabellaCode); $unitMillesimi = (float) ($tabellaMillesimi->millesimi ?? 0); $tabTotalMm = (float) ($domainTab?->totale_millesimi ?? 0); $ratio = ($tabTotalMm > 0) ? ($unitMillesimi / $tabTotalMm) : 0.0; // Importi reali per tabella e unità (prev_euro) se presenti $impRows = $importiPerTabella->get($tabellaCode) ?? collect(); $impProp = (float) $impRows ->filter(fn($r) => (string) ($r->ruolo_legacy ?? '') !== 'I') ->sum(fn($r) => (float) ($r->prev_euro ?? 0)); $impInq = (float) $impRows ->filter(fn($r) => (string) ($r->ruolo_legacy ?? '') === 'I') ->sum(fn($r) => (float) ($r->prev_euro ?? 0)); $impTot = $impProp + $impInq; $hasImporti = abs($impTot) > 0.00001; $percentInqFromImporti = $impTot != 0.0 ? ($impInq / $impTot) * 100 : 0.0; // Usa lo split importato solo quando c'è effettivamente una quota Inquilino. // In molti casi (es. tabelle ordinarie) lo staging fornisce solo la quota unità totale e lo split va applicato via percentuali voce. $useImportiSplit = $hasImporti && abs($impInq) > 0.00001; // Quota unità tabella: preferisci importi reali (consumi/individuali), altrimenti riparto millesimale $quotaUnitTabella = $hasImporti ? $impTot : ($totalPreventivo * $ratio); $rows = $items->map(function ($voce) use ($hasImporti, $useImportiSplit, $impInq, $quotaUnitTabella, $totalPreventivo, $ratio) { $voceCodice = $voce->codice ?? $voce->cod_voce ?? $voce->codice_voce ?? $voce->cod ?? null; $voceDescrizione = $voce->descrizione ?? $voce->descr ?? $voce->voce ?? $voce->voce_descrizione ?? ''; $importoVoce = (float) ($voce->preventivo_euro ?? 0); // Quota unità voce if ($hasImporti) { $share = ($totalPreventivo != 0.0) ? ($importoVoce / $totalPreventivo) : 0.0; $quotaUnita = $quotaUnitTabella * $share; if ($useImportiSplit) { $quotaInquilinoVoce = $impInq * $share; } else { $percInq = is_numeric($voce->perc_inquilino ?? null) ? (float) $voce->perc_inquilino : 0.0; $quotaInquilinoVoce = $quotaUnita * ($percInq / 100); } } else { $quotaUnita = $importoVoce * $ratio; $percInq = is_numeric($voce->perc_inquilino ?? null) ? (float) $voce->perc_inquilino : 0.0; $quotaInquilinoVoce = $quotaUnita * ($percInq / 100); } $quotaProprietarioVoce = $quotaUnita - $quotaInquilinoVoce; $percentualeInquilinoVoce = $quotaUnita != 0.0 ? ($quotaInquilinoVoce / $quotaUnita) * 100 : 0.0; return [ 'codice' => $voceCodice, 'descrizione' => $voceDescrizione, 'importo_preventivato' => $importoVoce, 'quota_unita' => $quotaUnita, 'importo_proprietario' => $quotaProprietarioVoce, 'importo_inquilino' => $quotaInquilinoVoce, 'percentuale_inquilino' => $percentualeInquilinoVoce, ]; })->values()->all(); // Totali tabella (prop/inq): // - se importi reali disponibili, usa quelli // - altrimenti deriva dalle percentuali voce if ($useImportiSplit) { $quotaProprietario = $impProp; $quotaInquilino = $impInq; $percentualeInquilino = ($impProp + $impInq) != 0.0 ? ($impInq / ($impProp + $impInq)) * 100 : 0.0; } else { $quotaProprietario = 0.0; $quotaInquilino = 0.0; foreach ($rows as $row) { $quotaProprietario += (float) ($row['importo_proprietario'] ?? 0); $quotaInquilino += (float) ($row['importo_inquilino'] ?? 0); } $percentualeInquilino = ($quotaProprietario + $quotaInquilino) != 0.0 ? ($quotaInquilino / ($quotaProprietario + $quotaInquilino)) * 100 : 0.0; } if (array_key_exists($tipoGestioneKey, $totaliGestione)) { $totaliGestione[$tipoGestioneKey] += ($quotaProprietario + $quotaInquilino); } return [ 'tabella_codice' => $tabellaCode, 'tabella_nome' => $tabellaNome, 'tipo' => $tipoGestione, 'total_preventivo' => $totalPreventivo, 'quota_unit' => $quotaUnitTabella, 'importo_proprietario' => $quotaProprietario, 'importo_inquilino' => $quotaInquilino, 'percentuale_inquilino' => $percentualeInquilino, 'tabella_millesimi' => $tabTotalMm, 'unit_millesimi' => $unitMillesimi, 'voci' => $rows, ]; }) ->values() ->all(); $this->totaliPerGestione = $totaliGestione; } }