> */ public array $vociSpesa = []; public string $presetRipartizione = 'manuale'; /** @var array */ public array $presetOptions = []; public ?int $presetId = null; public string $presetNome = ''; public bool $presetDirty = false; /** @var array> */ public array $tabelle = []; /** @var array> */ public array $righe = []; public ?array $tabellaInfo = null; public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public function mount(): void { $this->mountInteractsWithTable(); $user = Auth::user(); if (! $user instanceof User) { abort(403); } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make() ->title('Nessuno stabile disponibile') ->body('Seleziona uno stabile per vedere le tabelle millesimali.') ->warning() ->send(); return; } $this->stabileId = (int) $stabileId; $this->annoGestione = AnnoGestioneContext::resolveActiveAnno($user); $this->codiceStabile = Stabile::query() ->whereKey($this->stabileId) ->value('codice_stabile') ?: Stabile::query() ->whereKey($this->stabileId) ->value('codice_interno'); $requestedTab = request()->query('tab'); if (in_array($requestedTab, ['archivio', 'prospetto', 'voci'], true)) { $this->tab = $requestedTab; } $this->loadTabelle(); $requested = request()->integer('tabella_id') ?: null; $this->tabellaId = $this->pickTabellaId($requested); $this->loadDettaglioTabella(); $this->loadVociSpesa(); $this->loadPresetOptions(); } public function updatedTab(string $value): void { if ($value === 'prospetto') { $this->loadDettaglioTabella(); } if ($value === 'voci') { $this->loadVociSpesa(); } } public function updatedPresetRipartizione(string $value): void { $this->presetRipartizione = in_array($value, ['manuale', 'confedilizia', 'custom'], true) ? $value : 'manuale'; if ($this->presetRipartizione === 'confedilizia') { $this->applyPresetRipartizione(); } if ($this->presetRipartizione === 'custom') { $this->applyCustomPreset(); } } public function updatedPresetId($value): void { $this->presetId = is_numeric($value) ? (int) $value : null; if ($this->presetRipartizione === 'custom') { $this->applyCustomPreset(); } } protected function getTableQuery(): Builder { $user = Auth::user(); if (! $user instanceof User) { return TabellaMillesimale::query()->whereRaw('1 = 0'); } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return TabellaMillesimale::query()->whereRaw('1 = 0'); } $orderColumn = Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : (Schema::hasColumn('tabelle_millesimali', 'nord') ? 'nord' : (Schema::hasColumn('tabelle_millesimali', 'ordinamento') ? 'ordinamento' : 'id')); return TabellaMillesimale::query() ->where('stabile_id', $activeStabileId) // In UI il conteggio "Unità" deve riflettere quante unità hanno righe millesimi ->withCount([ 'dettagliMillesimali as unita_partecipanti_count' => function (Builder $q): void { $q ->where('partecipa', 1) ->where(function (Builder $w): void { $w->whereNull('ruolo_legacy')->orWhere('ruolo_legacy', 'C'); }) ->select(DB::raw('count(distinct unita_immobiliare_id)')); }, ]) // Nascondi le tabelle generate automaticamente da GESCON (es. *.ST) ->where(function (Builder $q) { $q->whereNull('descrizione')->orWhere('descrizione', 'not like', '%generata automaticamente%'); }) ->orderBy($orderColumn) ->orderBy('codice_tabella') ->orderBy('denominazione') ->orderBy('id'); } public function table(Table $table): Table { $gruppoOptions = [ 'O' => 'O (Ordinaria)', 'R' => 'R (Riscaldamento)', 'S' => 'S (Straordinaria)', ]; $calcoloOptions = [ 'millesimi' => 'MILLESIMI', 'a_consumo' => 'A CONSUMO', 'conguagli' => 'CONGUAGLI', 'personali' => 'PERSONALI', 'parti' => 'PARTI', 'fisso' => 'FISSO', ]; return $table ->striped() ->headerActions([ Action::make('create') ->label('Nuova tabella') ->icon('heroicon-o-plus') ->modalHeading('Nuova tabella millesimale') ->form([ TextInput::make('codice_tabella') ->label('Codice') ->required() ->maxLength(50), TextInput::make('denominazione') ->label('Descrizione') ->required() ->maxLength(255), Select::make('gruppo') ->label('Gruppo') ->options($gruppoOptions) ->default('O') ->required(), Select::make('tipo_calcolo') ->label('Calcolo') ->options($calcoloOptions) ->default('millesimi') ->required(), TextInput::make('totale_millesimi') ->label('Totale') ->numeric() ->default(1000) ->rules(['nullable', 'numeric', 'min:0']), TextInput::make('ordine_visualizzazione') ->label('Ord. (UI)') ->numeric() ->rules(['nullable', 'integer', 'min:0']), TextInput::make('ordinamento') ->label('Ordinamento') ->numeric() ->rules(['nullable', 'integer', 'min:0']) ->helperText('Fallback per compatibilità se Ord. (UI) non è usato.'), Toggle::make('attiva')->label('Attiva')->default(true), Textarea::make('note') ->label('Note') ->rows(3) ->maxLength(1000), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return; } $gruppo = strtoupper(trim((string) ($data['gruppo'] ?? 'O'))); $tipoTabella = match ($gruppo) { 'R' => 'riscaldamento', 'S' => 'straordinaria', default => 'ordinaria', }; $tipoCalcolo = strtolower(trim((string) ($data['tipo_calcolo'] ?? 'millesimi'))); $meta = [ 'tipo' => $gruppo, 'calcolo' => $tipoCalcolo, ]; $payload = [ 'stabile_id' => (int) $stabileId, 'codice_tabella' => strtoupper(trim((string) ($data['codice_tabella'] ?? ''))), 'denominazione' => trim((string) ($data['denominazione'] ?? '')), 'tipo_tabella' => $tipoTabella, 'tipo_calcolo' => $tipoCalcolo, 'totale_millesimi' => is_numeric($data['totale_millesimi'] ?? null) ? (float) $data['totale_millesimi'] : null, 'ordine_visualizzazione' => is_numeric($data['ordine_visualizzazione'] ?? null) ? (int) $data['ordine_visualizzazione'] : null, 'ordinamento' => is_numeric($data['ordinamento'] ?? null) ? (int) $data['ordinamento'] : null, 'attiva' => (bool) ($data['attiva'] ?? true), 'note' => trim((string) ($data['note'] ?? '')) ?: null, 'meta_legacy' => $meta, ]; if (Schema::hasColumn('tabelle_millesimali', 'creato_da')) { $payload['creato_da'] = (int) $user->id; } if (Schema::hasColumn('tabelle_millesimali', 'created_by')) { $payload['created_by'] = (int) $user->id; } TabellaMillesimale::query()->create($payload); }), ]) ->columns([ TextColumn::make( Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : 'ordinamento' ) ->label('Ord.') ->sortable() ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('codice_tabella') ->label('Codice') ->sortable() ->searchable() ->toggleable(), TextColumn::make('denominazione') ->label('Descrizione') ->getStateUsing(fn(TabellaMillesimale $record) => $record->denominazione ?: ($record->descrizione ?: ($record->nome_tabella ?: '—'))) ->sortable() ->searchable() ->wrap(), TextColumn::make('tipo_calcolo') ->label('Calcolo') ->getStateUsing(function (TabellaMillesimale $record) { $meta = is_array($record->meta_legacy) ? $record->meta_legacy : (is_string($record->meta_legacy) ? json_decode($record->meta_legacy, true) : null); $v = is_array($meta) ? ($meta['calcolo'] ?? null) : null; if (!$v) { $v = $record->tipo_calcolo; } if (!$v) return '—'; $v = strtolower((string) $v); return match ($v) { 'acqua', 'a', 'consumo', 'c', 'a_consumo' => 'A CONSUMO', 'millesimi', 'm' => 'MILLESIMI', 'conguagli', 'x' => 'CONGUAGLI', 'personali', 'p' => 'PERSONALI', 'parti' => 'PARTI', 'fisso' => 'FISSO', default => strtoupper((string) $v), }; }) ->toggleable(), TextColumn::make('tipo_tabella') ->label('Gruppo') ->getStateUsing(function (TabellaMillesimale $record) { $meta = is_array($record->meta_legacy) ? $record->meta_legacy : (is_string($record->meta_legacy) ? json_decode($record->meta_legacy, true) : null); $codice = strtoupper((string) ($record->codice_tabella ?? '')); $tipoCalcolo = is_array($meta) ? ($meta['calcolo'] ?? null) : null; $tipoCalcolo = $tipoCalcolo ?: $record->tipo_calcolo; $tipoCalcolo = strtolower((string) ($tipoCalcolo ?? '')); if ($codice === 'ACQUA' || in_array($tipoCalcolo, ['acqua', 'a', 'consumo', 'c', 'a_consumo'], true)) { return 'A (Acqua)'; } $ors = is_array($meta) ? ($meta['tipo'] ?? null) : null; if ($ors) { $u = strtoupper((string) $ors); return match ($u) { 'O' => 'O (Ordinaria)', 'R' => 'R (Riscaldamento)', 'S' => 'S (Straordinaria)', default => strtoupper((string) $ors), }; } $tipologia = strtoupper((string) ($record->tipologia ?? '')); if (in_array($tipologia, ['O', 'R', 'S'], true)) { return match ($tipologia) { 'R' => 'R (Riscaldamento)', 'S' => 'S (Straordinaria)', default => 'O (Ordinaria)', }; } $t = strtolower((string) ($record->tipo_tabella ?? '')); if ($t === 'riscaldamento') return 'R (Riscaldamento)'; if ($t === '') return '—'; return 'O (Ordinaria)'; }) ->sortable() ->toggleable(), TextColumn::make('unita_partecipanti_count') ->label('Unità') ->sortable() ->toggleable(), TextColumn::make('totale_millesimi') ->label('Totale') ->getStateUsing(fn(TabellaMillesimale $record) => $record->getRawOriginal('totale_millesimi')) ->numeric(decimalPlaces: 4) ->sortable() ->toggleable(), TextColumn::make('preventivo') ->label('Prev.') ->getStateUsing(function (TabellaMillesimale $record) { $meta = is_array($record->meta_legacy) ? $record->meta_legacy : (is_string($record->meta_legacy) ? json_decode($record->meta_legacy, true) : null); $val = is_array($meta) ? ($meta['tot_prev_euro'] ?? $meta['tot_prev'] ?? null) : null; return is_numeric($val) ? (float) $val : null; }) ->numeric(decimalPlaces: 2) ->alignRight() ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('consuntivo') ->label('Cons.') ->getStateUsing(function (TabellaMillesimale $record) { $meta = is_array($record->meta_legacy) ? $record->meta_legacy : (is_string($record->meta_legacy) ? json_decode($record->meta_legacy, true) : null); $val = is_array($meta) ? ($meta['tot_cons_euro'] ?? $meta['tot_cons'] ?? null) : null; return is_numeric($val) ? (float) $val : null; }) ->numeric(decimalPlaces: 2) ->alignRight() ->toggleable(isToggledHiddenByDefault: true), IconColumn::make('is_bilanciata') ->label('Bil.') ->boolean() ->state(function (TabellaMillesimale $record): bool { $raw = $record->getRawOriginal('totale_millesimi'); if (is_numeric($raw)) { return abs(((float) $raw) - 1000) < 0.01; } return (bool) $record->isBilanciata(); }) ->toggleable(), IconColumn::make('attiva') ->label('Attiva') ->boolean(), ]) ->actions([ Action::make('edit') ->label('Modifica') ->icon('heroicon-o-pencil-square') ->modalHeading('Modifica tabella millesimale') ->form([ TextInput::make('codice_tabella') ->label('Codice') ->required() ->maxLength(50), TextInput::make('denominazione') ->label('Descrizione') ->required() ->maxLength(255), Select::make('gruppo') ->label('Gruppo') ->options($gruppoOptions) ->required(), Select::make('tipo_calcolo') ->label('Calcolo') ->options($calcoloOptions) ->required(), TextInput::make('totale_millesimi') ->label('Totale') ->numeric() ->rules(['nullable', 'numeric', 'min:0']), TextInput::make('ordine_visualizzazione') ->label('Ord. (UI)') ->numeric() ->rules(['nullable', 'integer', 'min:0']), TextInput::make('ordinamento') ->label('Ordinamento') ->numeric() ->rules(['nullable', 'integer', 'min:0']), Toggle::make('attiva')->label('Attiva')->default(true), Textarea::make('note') ->label('Note') ->rows(3) ->maxLength(1000), ]) ->fillForm(function (TabellaMillesimale $record): array { $meta = is_array($record->meta_legacy) ? $record->meta_legacy : (is_string($record->meta_legacy) ? json_decode($record->meta_legacy, true) : null); $gruppo = is_array($meta) ? ($meta['tipo'] ?? null) : null; if (! $gruppo) { $t = strtolower((string) ($record->tipo_tabella ?? '')); $gruppo = $t === 'riscaldamento' ? 'R' : ($t === 'straordinaria' ? 'S' : 'O'); } $calcolo = is_array($meta) ? ($meta['calcolo'] ?? null) : null; if (! $calcolo) { $calcolo = $record->tipo_calcolo ?: 'millesimi'; } return [ 'codice_tabella' => (string) ($record->codice_tabella ?? ''), 'denominazione' => (string) ($record->denominazione ?? ($record->descrizione ?? '')), 'gruppo' => strtoupper((string) ($gruppo ?? 'O')), 'tipo_calcolo' => strtolower((string) ($calcolo ?? 'millesimi')), 'totale_millesimi' => $record->getRawOriginal('totale_millesimi'), 'ordine_visualizzazione' => $record->ordine_visualizzazione, 'ordinamento' => $record->ordinamento, 'attiva' => (bool) ($record->attiva ?? true), 'note' => (string) ($record->note ?? ''), ]; }) ->action(function (TabellaMillesimale $record, array $data): void { $user = Auth::user(); $gruppo = strtoupper(trim((string) ($data['gruppo'] ?? 'O'))); $tipoTabella = match ($gruppo) { 'R' => 'riscaldamento', 'S' => 'straordinaria', default => 'ordinaria', }; $tipoCalcolo = strtolower(trim((string) ($data['tipo_calcolo'] ?? 'millesimi'))); $meta = is_array($record->meta_legacy) ? $record->meta_legacy : (is_string($record->meta_legacy) ? json_decode($record->meta_legacy, true) : []); $meta = is_array($meta) ? $meta : []; $meta['tipo'] = $gruppo; $meta['calcolo'] = $tipoCalcolo; $payload = [ 'codice_tabella' => strtoupper(trim((string) ($data['codice_tabella'] ?? ''))), 'denominazione' => trim((string) ($data['denominazione'] ?? '')), 'tipo_tabella' => $tipoTabella, 'tipo_calcolo' => $tipoCalcolo, 'totale_millesimi' => is_numeric($data['totale_millesimi'] ?? null) ? (float) $data['totale_millesimi'] : null, 'ordine_visualizzazione' => is_numeric($data['ordine_visualizzazione'] ?? null) ? (int) $data['ordine_visualizzazione'] : null, 'ordinamento' => is_numeric($data['ordinamento'] ?? null) ? (int) $data['ordinamento'] : null, 'attiva' => (bool) ($data['attiva'] ?? true), 'note' => trim((string) ($data['note'] ?? '')) ?: null, 'meta_legacy' => $meta, ]; if ($user instanceof User && Schema::hasColumn('tabelle_millesimali', 'modificato_da')) { $payload['modificato_da'] = (int) $user->id; } $record->update($payload); }), Action::make('delete') ->label('Elimina') ->icon('heroicon-o-trash') ->color('danger') ->requiresConfirmation() ->action(fn(TabellaMillesimale $record) => $record->delete()), Action::make('prospetto') ->label('Prospetto') ->icon('heroicon-o-rectangle-group') ->url(fn(TabellaMillesimale $record) => static::getUrl(panel: 'admin-filament') . '?tab=prospetto&tabella_id=' . (int) $record->id), ]); } protected function loadTabelle(): void { $this->tabelle = []; if (! $this->stabileId) { return; } $q = TabellaMillesimale::query() ->perStabile($this->stabileId) ->when(Schema::hasColumn('tabelle_millesimali', 'anno_gestione') && $this->annoGestione, function ($qq) { $qq->where(function ($q2) { $q2->where('anno_gestione', $this->annoGestione)->orWhereNull('anno_gestione'); })->orderByRaw('CASE WHEN anno_gestione IS NULL THEN 1 ELSE 0 END'); }) ->ordinato(); $this->tabelle = $q ->get(['id', 'codice_tabella', 'nome_tabella', 'denominazione', 'tipo_tabella', 'tipo_calcolo', 'totale_millesimi', 'ordinamento', 'meta_legacy']) ->map(function (TabellaMillesimale $t) { $codice = $t->codice_tabella ?: ($t->nome_tabella ?: ('TAB ' . $t->id)); $nome = $t->denominazione ?: ($t->nome_tabella_millesimale ?: ($t->nome_tabella ?: 'Tabella')); $calcoloRaw = $this->resolveCalcoloRaw($t); $isConsumo = $this->isConsumoCalcolo($calcoloRaw); $calcoloLabel = $this->formatCalcoloLabel($calcoloRaw); $tipoLabel = $this->resolveTipoLabel($t, $calcoloRaw); $meta = is_array($t->meta_legacy) ? $t->meta_legacy : (is_string($t->meta_legacy) ? json_decode($t->meta_legacy, true) : null); $meta = is_array($meta) ? $meta : []; $prev = $meta['tot_prev_euro'] ?? $meta['tot_prev'] ?? null; $cons = $meta['tot_cons_euro'] ?? $meta['tot_cons'] ?? null; return [ 'id' => (int) $t->id, 'codice' => $codice, 'nome' => $nome, 'tipo' => $tipoLabel, 'calcolo' => $calcoloLabel, 'is_consumo' => $isConsumo, 'totale' => is_numeric($t->totale_millesimi) ? (float) $t->totale_millesimi : null, 'ordinamento' => (int) ($t->ordinamento ?? 999999), 'is_bilanciata' => (bool) ($t->is_bilanciata ?? false), 'preventivo' => is_numeric($prev) ? (float) $prev : null, 'consuntivo' => is_numeric($cons) ? (float) $cons : null, ]; }) ->values() ->all(); } protected function pickTabellaId(?int $candidate): ?int { if ($candidate && collect($this->tabelle)->contains(fn($t) => (int) $t['id'] === $candidate)) { return $candidate; } $first = $this->tabelle[0]['id'] ?? null; return is_numeric($first) ? (int) $first : null; } protected function loadDettaglioTabella(): void { $this->righe = []; $this->tabellaInfo = null; if (! $this->stabileId || ! $this->tabellaId) { return; } $tabella = TabellaMillesimale::query() ->perStabile($this->stabileId) ->with(['dettagliMillesimali.unitaImmobiliare', 'dettagliMillesimali.unitaImmobiliare.palazzinaObj']) ->when(Schema::hasColumn('tabelle_millesimali', 'anno_gestione') && $this->annoGestione, function ($qq) { $qq->where(function ($q2) { $q2->where('anno_gestione', $this->annoGestione)->orWhereNull('anno_gestione'); }); }) ->whereKey($this->tabellaId) ->first(); if (! $tabella) { return; } $rawTotale = $tabella->getRawOriginal('totale_millesimi'); $totaleCalcolato = (float) ($tabella->calcolaTotaleMillesimi() ?? 0); $totale = is_numeric($rawTotale) ? (float) $rawTotale : $totaleCalcolato; $codice = $tabella->codice_tabella ?: ($tabella->nome_tabella ?: ('TAB ' . $tabella->id)); $nome = $tabella->denominazione ?: ($tabella->nome_tabella_millesimale ?: ($tabella->nome_tabella ?: 'Tabella')); $isBilanciata = is_numeric($rawTotale) ? (abs(((float) $rawTotale) - 1000) < 0.01) : (bool) $tabella->isBilanciata(); $this->tabellaInfo = [ 'id' => (int) $tabella->id, 'codice' => $codice, 'nome' => $nome, 'tipo' => $this->resolveTipoLabel($tabella, $this->resolveCalcoloRaw($tabella)), 'calcolo' => $this->formatCalcoloLabel($this->resolveCalcoloRaw($tabella)), 'is_consumo' => $this->isConsumoCalcolo($this->resolveCalcoloRaw($tabella)), 'totale' => $totale, 'is_bilanciata' => $isBilanciata, 'preventivo' => $this->resolveLegacyImporto($tabella, 'tot_prev_euro', 'tot_prev'), 'consuntivo' => $this->resolveLegacyImporto($tabella, 'tot_cons_euro', 'tot_cons'), ]; $this->righe = $tabella->dettagliMillesimali ->filter(fn($d) => $d->unitaImmobiliare !== null) ->sortBy(function ($d) { $u = $d->unitaImmobiliare; return sprintf( '%s|%s|%s|%s|%010d', $u?->palazzina ?? '', $u?->scala ?? '', str_pad((string) ($u?->piano ?? 0), 6, '0', STR_PAD_LEFT), $u?->interno ?? '', (int) ($u?->id ?? 0) ); }) ->map(function ($d) use ($totale) { $u = $d->unitaImmobiliare; $millesimi = (float) ($d->millesimi ?? 0); $percentuale = $totale > 0 ? ($millesimi / $totale) * 100 : 0; return [ 'unita_id' => (int) ($u?->id ?? 0), 'codice_unita' => $u?->codice_unita ?? ($u?->codice_completo ?? null), 'codice_unita_display' => $this->formatCodiceUnita($u?->codice_unita ?? ($u?->codice_completo ?? null)), 'denominazione' => $u?->denominazione, 'palazzina' => $u?->palazzina, 'scala' => $u?->scala, 'piano' => $u?->piano, 'interno' => $u?->interno, 'millesimi' => $millesimi, 'percentuale' => round($percentuale, 4), 'partecipa' => (bool) ($d->partecipa ?? true), ]; }) ->values() ->all(); } protected function loadVociSpesa(): void { $this->vociSpesa = []; if (! $this->stabileId || ! $this->tabellaId) { return; } if (! Schema::hasTable('voci_spesa')) { return; } $hasCond = Schema::hasColumn('voci_spesa', 'percentuale_condomino'); $hasInq = Schema::hasColumn('voci_spesa', 'percentuale_inquilino'); $rows = VoceSpesa::query() ->where('stabile_id', $this->stabileId) ->where('tabella_millesimale_default_id', $this->tabellaId) ->orderBy('ordinamento') ->orderBy('descrizione') ->get(); $this->vociSpesa = $rows->map(function (VoceSpesa $v) use ($hasCond, $hasInq): array { return [ 'id' => (int) $v->id, 'codice' => $v->codice, 'descrizione' => $v->descrizione, 'categoria' => $v->categoria, 'tipo_gestione' => $v->tipo_gestione, 'percentuale_condomino' => $hasCond ? (float) ($v->percentuale_condomino ?? 0) : null, 'percentuale_inquilino' => $hasInq ? (float) ($v->percentuale_inquilino ?? 0) : null, 'attiva' => (bool) ($v->attiva ?? true), 'modifica' => false, ]; })->all(); $this->presetDirty = false; } protected function loadPresetOptions(): void { $this->presetOptions = []; if (! $this->stabileId) { return; } if (! Schema::hasTable('ripartizione_presets')) { return; } $this->presetOptions = RipartizionePreset::query() ->where('stabile_id', $this->stabileId) ->orderBy('nome') ->pluck('nome', 'id') ->map(fn($v) => (string) $v) ->all(); } public function applyPresetRipartizione(): void { if ($this->presetRipartizione === 'manuale') { return; } if ($this->presetRipartizione !== 'confedilizia') { return; } foreach ($this->vociSpesa as $i => $voce) { $categoria = $this->mapVoceCategoriaToConfedilizia((string) ($voce['categoria'] ?? '')); $default = RipartizioneSpeseInquilini::getDefaultConfedilizia($categoria); $this->vociSpesa[$i]['percentuale_condomino'] = (float) ($default['proprietario'] ?? 100); $this->vociSpesa[$i]['percentuale_inquilino'] = (float) ($default['inquilino'] ?? 0); $this->vociSpesa[$i]['modifica'] = true; } $this->presetDirty = true; } public function applyCustomPreset(): void { if (! $this->presetId || ! Schema::hasTable('ripartizione_presets')) { return; } $preset = RipartizionePreset::query() ->where('stabile_id', $this->stabileId) ->whereKey($this->presetId) ->first(); if (! $preset) { return; } $data = is_array($preset->dati) ? $preset->dati : []; $map = []; foreach ($data as $row) { $codice = isset($row['codice']) ? (string) $row['codice'] : ''; if ($codice !== '') { $map[$codice] = $row; } } foreach ($this->vociSpesa as $i => $voce) { $codice = (string) ($voce['codice'] ?? ''); if ($codice === '' || ! isset($map[$codice])) { continue; } $row = $map[$codice]; $cond = isset($row['percentuale_condomino']) && is_numeric($row['percentuale_condomino']) ? (float) $row['percentuale_condomino'] : 100.0; $inq = isset($row['percentuale_inquilino']) && is_numeric($row['percentuale_inquilino']) ? (float) $row['percentuale_inquilino'] : 0.0; $this->vociSpesa[$i]['percentuale_condomino'] = $cond; $this->vociSpesa[$i]['percentuale_inquilino'] = $inq; $this->vociSpesa[$i]['modifica'] = true; } $this->presetDirty = false; } public function savePresetAsNew(): void { if (! $this->stabileId || ! Schema::hasTable('ripartizione_presets')) { return; } $nome = trim($this->presetNome); if ($nome === '') { Notification::make()->title('Inserisci un nome per il preset')->warning()->send(); return; } $data = array_values(array_map(function ($row): array { return [ 'codice' => $row['codice'] ?? null, 'percentuale_condomino' => is_numeric($row['percentuale_condomino'] ?? null) ? (float) $row['percentuale_condomino'] : null, 'percentuale_inquilino' => is_numeric($row['percentuale_inquilino'] ?? null) ? (float) $row['percentuale_inquilino'] : null, ]; }, $this->vociSpesa)); $preset = RipartizionePreset::query()->create([ 'stabile_id' => $this->stabileId, 'nome' => $nome, 'dati' => $data, ]); $this->presetNome = ''; $this->loadPresetOptions(); $this->presetId = (int) $preset->id; $this->presetRipartizione = 'custom'; $this->presetDirty = false; Notification::make()->title('Preset creato')->success()->send(); } public function updatePreset(): void { if (! $this->stabileId || ! $this->presetId || ! Schema::hasTable('ripartizione_presets')) { return; } $preset = RipartizionePreset::query() ->where('stabile_id', $this->stabileId) ->whereKey($this->presetId) ->first(); if (! $preset) { return; } $data = array_values(array_map(function ($row): array { return [ 'codice' => $row['codice'] ?? null, 'percentuale_condomino' => is_numeric($row['percentuale_condomino'] ?? null) ? (float) $row['percentuale_condomino'] : null, 'percentuale_inquilino' => is_numeric($row['percentuale_inquilino'] ?? null) ? (float) $row['percentuale_inquilino'] : null, ]; }, $this->vociSpesa)); $preset->update(['dati' => $data]); $this->presetDirty = false; Notification::make()->title('Preset aggiornato')->success()->send(); } public function updatedVociSpesa($value, string $name): void { if (! str_contains($name, 'percentuale_')) { return; } if (! preg_match('/^vociSpesa\.(\d+)\.(percentuale_condomino|percentuale_inquilino)$/', $name, $m)) { return; } $index = (int) $m[1]; if (! isset($this->vociSpesa[$index])) { return; } if (empty($this->vociSpesa[$index]['modifica'])) { return; } $val = is_numeric($value) ? (float) $value : 0.0; $val = max(0, min(100, $val)); if ($m[2] === 'percentuale_condomino') { $this->vociSpesa[$index]['percentuale_condomino'] = $val; $this->vociSpesa[$index]['percentuale_inquilino'] = round(100 - $val, 2); } else { $this->vociSpesa[$index]['percentuale_inquilino'] = $val; $this->vociSpesa[$index]['percentuale_condomino'] = round(100 - $val, 2); } if ($this->presetRipartizione === 'custom' && $this->presetId) { $this->presetDirty = true; } } private function mapVoceCategoriaToConfedilizia(string $categoriaVoce): string { return match ($categoriaVoce) { 'riscaldamento' => 'B', 'ascensore' => 'C', 'illuminazione' => 'D', 'pulizia' => 'E', 'acqua' => 'G', 'energia_elettrica' => 'L', 'gas' => 'B', 'giardino_verde' => 'I', 'sicurezza' => 'N', 'amministrazione' => 'O', 'assicurazioni' => 'A', 'tasse_tributi' => 'O', 'spese_legali' => 'O', 'manutenzione_ordinaria' => 'A', 'manutenzione_straordinaria' => 'A', 'lavori_miglioramento' => 'A', default => 'A', }; } public function saveVociSpesa(): void { if (! $this->stabileId || ! $this->tabellaId) { return; } if (! Schema::hasTable('voci_spesa')) { return; } $hasCond = Schema::hasColumn('voci_spesa', 'percentuale_condomino'); $hasInq = Schema::hasColumn('voci_spesa', 'percentuale_inquilino'); $validated = Validator::make( ['voci' => $this->vociSpesa], [ 'voci' => ['array'], 'voci.*.id' => ['required', 'integer', 'min:1'], 'voci.*.percentuale_condomino' => ['nullable', 'numeric', 'min:0', 'max:100'], 'voci.*.percentuale_inquilino' => ['nullable', 'numeric', 'min:0', 'max:100'], ] )->validate(); $rows = (array) ($validated['voci'] ?? []); if ($rows === []) { return; } foreach ($rows as $r) { $id = (int) ($r['id'] ?? 0); if ($id <= 0) { continue; } if (empty($r['modifica'])) { continue; } $payload = []; if ($hasCond && array_key_exists('percentuale_condomino', $r)) { $payload['percentuale_condomino'] = is_numeric($r['percentuale_condomino']) ? (float) $r['percentuale_condomino'] : null; } if ($hasInq && array_key_exists('percentuale_inquilino', $r)) { $payload['percentuale_inquilino'] = is_numeric($r['percentuale_inquilino']) ? (float) $r['percentuale_inquilino'] : null; } if ($payload === []) { continue; } VoceSpesa::query() ->where('stabile_id', $this->stabileId) ->where('tabella_millesimale_default_id', $this->tabellaId) ->whereKey($id) ->update($payload); } $this->loadVociSpesa(); Notification::make() ->title('Voci aggiornate') ->success() ->send(); if ($this->presetRipartizione === 'custom' && $this->presetId) { $this->presetDirty = true; Notification::make() ->title('Preset modificato') ->body('Aggiorna il preset selezionato o creane uno nuovo.') ->warning() ->send(); } } private function formatCodiceUnita(?string $codice): ?string { if (! is_string($codice)) { return null; } $codice = trim($codice); if ($codice === '') { return null; } $prefix = is_string($this->codiceStabile) ? trim($this->codiceStabile) : ''; if ($prefix !== '') { $pattern = '/^' . preg_quote($prefix, '/') . '[\s\-\/]+/'; $codice = preg_replace($pattern, '', $codice) ?? $codice; } return $codice; } public function inizializzaRighe(): void { if (! $this->stabileId || ! $this->tabellaId) { return; } if (! Schema::hasTable('dettaglio_millesimi')) { Notification::make() ->title('Tabella dettagli non disponibile') ->body('La tabella dettaglio_millesimi non esiste nel database.') ->danger() ->send(); return; } $unitaIds = UnitaImmobiliare::query() ->where('stabile_id', $this->stabileId) ->orderBy('id') ->pluck('id') ->map(fn($v) => (int) $v) ->all(); if ($unitaIds === []) { Notification::make() ->title('Nessuna unità') ->body('Non ci sono unità immobiliari nello stabile attivo.') ->warning() ->send(); return; } DB::transaction(function () use ($unitaIds): void { $existing = DettaglioMillesimi::query() ->where('tabella_millesimale_id', $this->tabellaId) ->whereIn('unita_immobiliare_id', $unitaIds) ->pluck('unita_immobiliare_id') ->map(fn($v) => (int) $v) ->all(); $missing = array_values(array_diff($unitaIds, $existing)); if ($missing === []) { return; } $userId = Auth::id(); $rows = array_map(function (int $unitaId) use ($userId): array { return [ 'tabella_millesimale_id' => (int) $this->tabellaId, 'unita_immobiliare_id' => $unitaId, 'millesimi' => 0, 'partecipa' => 1, 'created_by' => $userId, 'created_at' => now(), 'updated_at' => now(), ]; }, $missing); DB::table('dettaglio_millesimi')->insert($rows); }); $this->loadDettaglioTabella(); Notification::make() ->title('Righe inizializzate') ->success() ->send(); } public function saveRighe(): void { if (! $this->stabileId || ! $this->tabellaId) { return; } $validated = Validator::make( ['righe' => $this->righe], [ 'righe' => ['array'], 'righe.*.unita_id' => ['required', 'integer', 'min:1'], 'righe.*.millesimi' => ['required', 'numeric', 'min:0'], 'righe.*.partecipa' => ['nullable', 'boolean'], ] )->validate(); /** @var array> $rows */ $rows = (array) ($validated['righe'] ?? []); $unitaIds = array_values(array_unique(array_map(fn($r) => (int) ($r['unita_id'] ?? 0), $rows))); $unitaIds = array_values(array_filter($unitaIds, fn(int $v) => $v > 0)); if ($unitaIds === []) { return; } $allowedUnita = UnitaImmobiliare::query() ->where('stabile_id', $this->stabileId) ->whereIn('id', $unitaIds) ->pluck('id') ->map(fn($v) => (int) $v) ->all(); $allowedSet = array_fill_keys($allowedUnita, true); $now = now(); $userId = Auth::id(); DB::transaction(function () use ($rows, $allowedSet, $now, $userId): void { foreach ($rows as $r) { $unitaId = (int) ($r['unita_id'] ?? 0); if ($unitaId <= 0 || ! isset($allowedSet[$unitaId])) { continue; } $millesimi = (float) ($r['millesimi'] ?? 0); $partecipa = ! empty($r['partecipa']) ? 1 : 0; $exists = DettaglioMillesimi::query() ->where('tabella_millesimale_id', $this->tabellaId) ->where('unita_immobiliare_id', $unitaId) ->first(); if ($exists) { $exists->update([ 'millesimi' => $millesimi, 'partecipa' => $partecipa, ]); continue; } DettaglioMillesimi::query()->create([ 'tabella_millesimale_id' => (int) $this->tabellaId, 'unita_immobiliare_id' => $unitaId, 'millesimi' => $millesimi, 'partecipa' => $partecipa, 'created_by' => $userId, 'created_at' => $now, 'updated_at' => $now, ]); } }); $totale = (float) DettaglioMillesimi::query() ->where('tabella_millesimale_id', $this->tabellaId) ->sum('millesimi'); TabellaMillesimale::query() ->whereKey($this->tabellaId) ->update(['totale_millesimi' => $totale]); $this->loadDettaglioTabella(); $this->loadTabelle(); Notification::make() ->title('Millesimi salvati') ->success() ->send(); } private function resolveCalcoloRaw(TabellaMillesimale $t): ?string { $meta = is_array($t->meta_legacy) ? $t->meta_legacy : (is_string($t->meta_legacy) ? json_decode($t->meta_legacy, true) : null); $v = is_array($meta) ? ($meta['calcolo'] ?? null) : null; if (! $v) { $v = $t->tipo_calcolo; } $v = strtolower(trim((string) ($v ?? ''))); if ($v === '') { return null; } return match ($v) { 'm' => 'millesimi', 'a', 'c' => 'a_consumo', 'x' => 'conguagli', 'p' => 'personali', default => $v, }; } private function formatCalcoloLabel(?string $calcolo): ?string { $c = strtolower(trim((string) ($calcolo ?? ''))); if ($c === '') { return null; } return match ($c) { 'acqua', 'a', 'consumo', 'c', 'a_consumo' => 'A CONSUMO', 'millesimi', 'm' => 'MILLESIMI', 'conguagli', 'x' => 'CONGUAGLI', 'personali', 'p' => 'PERSONALI', 'parti' => 'PARTI', 'fisso' => 'FISSO', default => strtoupper($c), }; } private function resolveTipoLabel(TabellaMillesimale $t, ?string $calcoloRaw): ?string { $meta = is_array($t->meta_legacy) ? $t->meta_legacy : (is_string($t->meta_legacy) ? json_decode($t->meta_legacy, true) : null); $meta = is_array($meta) ? $meta : []; $codice = strtoupper((string) ($t->codice_tabella ?? '')); if ($codice === 'ACQUA' || $this->isConsumoCalcolo($calcoloRaw)) { return 'A (Acqua)'; } $ors = isset($meta['tipo']) ? strtoupper((string) $meta['tipo']) : null; if ($ors) { return match ($ors) { 'O' => 'O (Ordinaria)', 'R' => 'R (Riscaldamento)', 'S' => 'S (Straordinaria)', default => $ors, }; } $tipologia = strtoupper((string) ($t->tipologia ?? '')); if (in_array($tipologia, ['O', 'R', 'S'], true)) { return match ($tipologia) { 'R' => 'R (Riscaldamento)', 'S' => 'S (Straordinaria)', default => 'O (Ordinaria)', }; } $tipo = strtolower((string) ($t->tipo_tabella ?? '')); if ($tipo === 'riscaldamento') return 'R (Riscaldamento)'; if ($tipo === 'straordinaria') return 'S (Straordinaria)'; if ($tipo === 'ordinaria') return 'O (Ordinaria)'; return $tipo !== '' ? strtoupper($tipo) : null; } private function resolveLegacyImporto(TabellaMillesimale $t, string $primary, string $fallback): ?float { $meta = is_array($t->meta_legacy) ? $t->meta_legacy : (is_string($t->meta_legacy) ? json_decode($t->meta_legacy, true) : null); $meta = is_array($meta) ? $meta : []; $value = $meta[$primary] ?? $meta[$fallback] ?? null; return is_numeric($value) ? (float) $value : null; } private function isConsumoCalcolo(?string $calcolo): bool { $c = strtolower(trim((string) ($calcolo ?? ''))); return in_array($c, ['acqua', 'a', 'consumo', 'c', 'a_consumo'], true); } }