netgescon-day0/app/Filament/Pages/UnitaImmobiliarePage.php

2498 lines
100 KiB
PHP

<?php
namespace App\Filament\Pages;
use App\Filament\Pages\Gescon\RubricaUniversaleScheda;
use App\Models\DettaglioRipartizioneSpese;
use App\Models\GestioneContabile;
use App\Models\Incasso;
use App\Models\Persona;
use App\Models\Proprieta;
use App\Models\RataEmessaNg;
use App\Models\RubricaRuolo;
use App\Models\RubricaUniversale;
use App\Models\UnitaImmobiliare;
use App\Models\UnitaRecapitoServizio;
use App\Models\User;
use App\Models\VoceSpesa;
use App\Services\Comunicazioni\RecapitiServizioResolver;
use App\Support\AnnoGestioneContext;
use App\Support\StabileContext;
use BackedEnum;
use Carbon\Carbon;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Hidden;
use Filament\Forms\Components\Repeater;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Schemas\Schema;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema as DbSchema;
use UnitEnum;
class UnitaImmobiliarePage extends Page implements HasForms
{
use InteractsWithForms;
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->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<int, string>
*/
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<int, string>
*/
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<int, string>
*/
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;
}
}