*/ protected array $tabellaAnnoCache = []; public static function canAccess(): bool { $user = Auth::user(); if (! $user instanceof User) { return false; } if ($user->hasAnyRole(['super-admin', 'admin'])) { return true; } return $user->can('contabilita.voci-spesa'); } public function mount(): void { $tab = (string) request()->query('tab', 'ordinaria'); $this->tipoGestioneTab = in_array($tab, ['ordinaria', 'acqua', 'riscaldamento', 'straordinaria'], true) ? $tab : 'ordinaria'; $this->gestioneContabileId = $this->resolveGestioneContabileId(); $preset = (string) request()->query('preset', 'manuale'); $this->presetRipartizione = in_array($preset, ['manuale', 'confedilizia'], true) ? $preset : 'manuale'; $focus = request()->query('tabella'); $this->tabellaFocusId = is_numeric($focus) ? (int) $focus : null; if (! $this->tabellaFocusId || $this->tabellaFocusId <= 0) { $this->tabellaFocusId = $this->getFirstTabellaIdForFocus(); } $this->mountInteractsWithTable(); } protected function getHeaderActions(): array { return [ Action::make('tabelle') ->label('Tabelle millesimali') ->icon('heroicon-o-rectangle-stack') ->url(TabelleMillesimaliProspetto::getUrl(panel: 'admin-filament')), Action::make('importaVociDaAnno') ->label('Importa voci da anno') ->icon('heroicon-o-arrow-down-tray') ->modalHeading('Importa voci per la gestione attiva') ->form([ Select::make('anno_sorgente') ->label('Anno sorgente') ->options(function (): array { $y = (int) now()->year; $out = []; for ($i = 0; $i < 8; $i++) { $out[(string) ($y - $i)] = (string) ($y - $i); } return $out; }) ->default(function (): string { $user = Auth::user(); $anno = $user instanceof User ? AnnoGestioneContext::resolveActiveAnno($user) : (int) now()->year; return (string) max(2000, $anno - 1); }) ->required(), Toggle::make('aggiorna_esistenti') ->label('Aggiorna anche le voci già presenti (ricollega tabella se serve)') ->default(true), ]) ->action(function (array $data): void { $annoSorgente = is_numeric($data['anno_sorgente'] ?? null) ? (int) $data['anno_sorgente'] : 0; $aggiorna = (bool) ($data['aggiorna_esistenti'] ?? true); $this->importaVociPerAnno($annoSorgente, $aggiorna); }), Action::make('ricollegaTabelleAnno') ->label('Ricollega tabelle (anno)') ->icon('heroicon-o-link') ->modalHeading('Ricollega tabelle all’anno attivo') ->requiresConfirmation() ->action(function (): void { $this->ricollegaTabellePerAnnoAttivo(); }), ]; } private function resolveGestioneIdForAnnoTipo(int $stabileId, int $anno, string $tipo): ?int { if (! Schema::hasTable('gestioni_contabili')) { return null; } $q = GestioneContabile::query() ->where('stabile_id', $stabileId) ->where('anno_gestione', $anno) ->where('tipo_gestione', $tipo) ->orderByDesc('gestione_attiva') ->orderByDesc('id'); $id = $q->value('id'); return is_numeric($id) ? (int) $id : null; } private function mapTabellaToAnno(?int $sourceTabellaId, int $stabileId, int $targetAnno, string $tipoGestione): ?int { if (! $sourceTabellaId || $sourceTabellaId <= 0) { return null; } /** @var TabellaMillesimale|null $source */ $source = TabellaMillesimale::query()->where('stabile_id', $stabileId)->whereKey($sourceTabellaId)->first(); if (! $source) { return null; } $codice = trim((string) ($source->codice_tabella ?? '')); if ($codice === '') { return $sourceTabellaId; } $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $q = TabellaMillesimale::query() ->where('stabile_id', $stabileId) ->where('codice_tabella', $codice); if ($hasLegacyTipo) { $q->where('tipo', $tipoGestione); } if ($hasAnno) { $q->where('anno_gestione', $targetAnno); } $id = $q->orderBy('ordine_visualizzazione') ->orderBy('ordinamento') ->orderBy('id') ->value('id'); return is_numeric($id) ? (int) $id : $sourceTabellaId; } private function importaVociPerAnno(int $annoSorgente, bool $aggiornaEsistenti = true): void { $user = Auth::user(); if (! $user instanceof User) { return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make()->title('Seleziona uno stabile')->warning()->send(); return; } $tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria'); $annoTarget = AnnoGestioneContext::resolveActiveAnno($user); $gestioneTargetId = $this->gestioneContabileId ?: $this->resolveGestioneContabileId(); if (! $gestioneTargetId) { Notification::make()->title('Gestione target non trovata')->danger()->send(); return; } $gestioneSourceId = $this->resolveGestioneIdForAnnoTipo($stabileId, $annoSorgente, $tipo); if (! $gestioneSourceId) { Notification::make() ->title('Gestione sorgente non trovata') ->body('Non esiste una gestione per ' . $annoSorgente . ' (' . strtoupper($tipo) . ').') ->warning() ->send(); return; } $sourceVoci = VoceSpesa::query() ->where('stabile_id', $stabileId) ->where('gestione_contabile_id', $gestioneSourceId) ->orderBy('ordinamento') ->orderBy('codice') ->get(); if ($sourceVoci->isEmpty()) { Notification::make()->title('Nessuna voce da importare')->warning()->send(); return; } $existing = VoceSpesa::query() ->where('stabile_id', $stabileId) ->where('gestione_contabile_id', $gestioneTargetId) ->pluck('id', 'codice') ->all(); $created = 0; $updated = 0; $skipped = 0; DB::transaction(function () use ( $sourceVoci, $existing, $stabileId, $gestioneTargetId, $annoTarget, $tipo, $aggiornaEsistenti, &$created, &$updated, &$skipped, ): void { foreach ($sourceVoci as $src) { if (! $src instanceof VoceSpesa) { continue; } $codice = trim((string) ($src->codice ?? '')); if ($codice === '') { $skipped++; continue; } $targetTabellaId = $this->mapTabellaToAnno( is_numeric($src->tabella_millesimale_default_id ?? null) ? (int) $src->tabella_millesimale_default_id : null, $stabileId, $annoTarget, $tipo, ); if (isset($existing[$codice])) { if (! $aggiornaEsistenti) { $skipped++; continue; } $targetId = (int) $existing[$codice]; $target = VoceSpesa::query()->whereKey($targetId)->first(); if (! $target) { $skipped++; continue; } $patch = []; if (Schema::hasColumn('voci_spesa', 'tabella_millesimale_default_id')) { if ((int) ($target->tabella_millesimale_default_id ?? 0) !== (int) ($targetTabellaId ?? 0)) { $patch['tabella_millesimale_default_id'] = $targetTabellaId; } } if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) { if ((string) ($target->tipo_gestione ?? '') !== $tipo) { $patch['tipo_gestione'] = $tipo; } } if ($patch !== []) { $target->update($patch); $updated++; } else { $skipped++; } continue; } // Crea nuova voce per gestione target (preserva codice). $data = $src->toArray(); unset($data['id'], $data['id_voce'], $data['created_at'], $data['updated_at']); $data['stabile_id'] = $stabileId; $data['gestione_contabile_id'] = $gestioneTargetId; if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) { $data['tipo_gestione'] = $tipo; } if (Schema::hasColumn('voci_spesa', 'tabella_millesimale_default_id')) { $data['tabella_millesimale_default_id'] = $targetTabellaId; } VoceSpesa::query()->create($data); $created++; } }); Notification::make() ->title('Import voci completato') ->body('Create: ' . $created . ' · Aggiornate: ' . $updated . ' · Saltate: ' . $skipped) ->success() ->send(); $this->dispatch('$refresh'); } private function ricollegaTabellePerAnnoAttivo(): void { $user = Auth::user(); if (! $user instanceof User) { return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { Notification::make()->title('Seleziona uno stabile')->warning()->send(); return; } $annoTarget = AnnoGestioneContext::resolveActiveAnno($user); $tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria'); $gestioneTargetId = $this->gestioneContabileId ?: $this->resolveGestioneContabileId(); if (! $gestioneTargetId) { Notification::make()->title('Gestione non trovata')->danger()->send(); return; } if (! Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) { Notification::make()->title('Anno tabelle non attivo')->body('La colonna anno_gestione non è presente sulle tabelle.')->warning()->send(); return; } $voci = VoceSpesa::query() ->where('stabile_id', $stabileId) ->where('gestione_contabile_id', $gestioneTargetId) ->whereNotNull('tabella_millesimale_default_id') ->with('tabellaMillesimaleDefault') ->get(); $updated = 0; DB::transaction(function () use ($voci, $stabileId, $annoTarget, $tipo, &$updated): void { foreach ($voci as $voce) { if (! $voce instanceof VoceSpesa) { continue; } $t = $voce->tabellaMillesimaleDefault; if (! $t) { continue; } $tabAnno = is_numeric($t->anno_gestione ?? null) ? (int) $t->anno_gestione : null; if ($tabAnno === null || $tabAnno === $annoTarget) { continue; } $newId = $this->mapTabellaToAnno((int) $t->id, $stabileId, $annoTarget, $tipo); if ($newId && $newId !== (int) $t->id) { $voce->update(['tabella_millesimale_default_id' => $newId]); $updated++; } } }); Notification::make() ->title('Ricollegamento completato') ->body('Voci aggiornate: ' . $updated) ->success() ->send(); $this->dispatch('$refresh'); } private function getFirstTabellaIdForFocus(): ?int { $opts = $this->getTabelleMillesimaliOptions(); if (empty($opts)) { return null; } $firstKey = array_key_first($opts); return is_numeric($firstKey) ? (int) $firstKey : null; } /** @return array */ public function getTabelleMillesimaliOptions(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return []; } $tipoTab = $this->tipoGestioneTab ?: 'ordinaria'; $tipo = $this->normalizeTipoGestione($tipoTab); $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); $anno = AnnoGestioneContext::resolveActiveAnno($user); $q = TabellaMillesimale::query() ->where('stabile_id', $stabileId) ->orderBy('ordine_visualizzazione') ->orderBy('ordinamento') ->orderBy('codice_tabella') ->orderBy('nome_tabella'); if ($hasAnno) { $q->where('anno_gestione', $anno); } if ($hasLegacyTipo) { $q->whereIn('tipo', $this->getLegacyTipoValues($tipo)); } return $q ->get(['id', 'codice_tabella', 'denominazione', 'nome_tabella']) ->mapWithKeys(function (TabellaMillesimale $t): array { $code = trim((string) ($t->codice_tabella ?? '')); $name = trim((string) ($t->denominazione ?? '')); if ($name === '') { $name = trim((string) ($t->nome_tabella ?? '')); } if ($name === '') { $name = trim((string) ($t->nome_tabella_millesimale ?? '')); } $label = $this->formatTabellaLabel($code, $name, $t->id); return [(int) $t->id => $label]; }) ->all(); } public function updatedTabellaFocusId($value): void { $id = is_numeric($value) ? (int) $value : null; $this->tabellaFocusId = $id && $id > 0 ? $id : null; } /** @return array{label: string, count: int, totale_prev: float, totale_cons: float, voci: array} */ public function getDettaglioTabellaFocus(): array { $id = (int) ($this->tabellaFocusId ?? 0); $opts = $this->getTabelleMillesimaliOptions(); $label = $id > 0 && array_key_exists($id, $opts) ? (string) $opts[$id] : '—'; if ($id <= 0) { return ['label' => $label, 'count' => 0, 'totale_prev' => 0.0, 'totale_cons' => 0.0, 'voci' => []]; } $q = $this->getTableQuery() ->where('voci_spesa.tabella_millesimale_default_id', $id); $records = $q->get([ 'voci_spesa.codice', 'voci_spesa.descrizione', 'voci_spesa.importo_default', 'voci_spesa.importo_consuntivo', 'voci_spesa.percentuale_condomino', 'voci_spesa.percentuale_inquilino', 'voci_spesa.attiva', ]); $voci = $records->map(function ($r): array { return [ 'codice' => (string) ($r->codice ?? ''), 'descrizione' => (string) ($r->descrizione ?? ''), 'prev' => (float) ($r->importo_default ?? 0), 'cons' => (float) ($r->importo_consuntivo ?? 0), 'prop' => (float) ($r->percentuale_condomino ?? 0), 'inq' => (float) ($r->percentuale_inquilino ?? 0), 'attiva' => (bool) ($r->attiva ?? false), ]; })->all(); $totPrev = (float) $records->sum(fn($r) => (float) ($r->importo_default ?? 0)); $totCons = (float) $records->sum(fn($r) => (float) ($r->importo_consuntivo ?? 0)); return [ 'label' => $label, 'count' => count($voci), 'totale_prev' => $totPrev, 'totale_cons' => $totCons, 'voci' => $voci, ]; } public function getNoTabellaCount(): int { return (int) $this->getTableQuery() ->whereNull('voci_spesa.tabella_millesimale_default_id') ->count(); } /** * Quando cambia il preset dalla UI, applica eventuali regole. */ public function updatedPresetRipartizione(string $value): void { $this->presetRipartizione = in_array($value, ['manuale', 'confedilizia'], true) ? $value : 'manuale'; $this->applyPresetRipartizione(); } public function updatedGestioneContabileId($value): void { $id = is_numeric($value) ? (int) $value : null; $this->gestioneContabileId = $id && $id > 0 ? $id : null; $this->resetPage(); } public function getGestioniOptions(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId || ! Schema::hasTable('gestioni_contabili')) { return []; } $tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria'); $gestioni = GestioneContabile::query() ->where('stabile_id', $stabileId) ->where('tipo_gestione', $tipo) ->orderByDesc('gestione_attiva') ->orderByDesc('stato') ->orderByDesc('anno_gestione') ->orderByDesc('id') ->get(['id', 'anno_gestione', 'tipo_gestione', 'stato']) ->all(); $options = []; $seenYears = []; foreach ($gestioni as $g) { $anno = (string) ($g->anno_gestione ?? ''); if ($anno === '' || isset($seenYears[$anno])) { continue; } $seenYears[$anno] = true; $options[(int) $g->id] = $anno; } return $options; } private function normalizeTipoGestione(string $tipo): string { return $tipo === 'acqua' ? 'ordinaria' : $tipo; } /** @return array */ private function getLegacyTipoValues(string $tipo): array { return match ($tipo) { 'ordinaria' => ['ordinaria', 'O', 'o'], 'riscaldamento' => ['riscaldamento', 'R', 'r'], 'straordinaria' => ['straordinaria', 'S', 's'], default => [$tipo], }; } private function applyTipoGestioneFilter(Builder $q, string $tipoTab, bool $hasLegacyTipo): Builder { $tipo = $this->normalizeTipoGestione($tipoTab); if (Schema::hasColumn('voci_spesa', 'tipo_gestione')) { $q->where('voci_spesa.tipo_gestione', $tipo); } if ($tipoTab === 'acqua') { $q->where(function (Builder $qq) { $qq->whereRaw('UPPER(TRIM(tm.codice_tabella)) = ?', ['ACQUA']) ->orWhere('voci_spesa.categoria', 'acqua'); }); } elseif ($tipoTab === 'ordinaria') { $q->whereRaw('(tm.codice_tabella IS NULL OR UPPER(TRIM(tm.codice_tabella)) != ?)', ['ACQUA']); $q->where(function (Builder $qq) { $qq->whereNull('voci_spesa.categoria')->orWhere('voci_spesa.categoria', '!=', 'acqua'); }); } return $q; } private function getTabellaGroupTitle(VoceSpesa $record): string { $tabella = $this->resolveTabellaForAnno($record->tabellaMillesimaleDefault, (int) $record->stabile_id); $code = trim((string) ($tabella?->codice_tabella ?? '')); $name = trim((string) ($tabella?->denominazione ?? '')); if ($name === '') { $name = trim((string) ($tabella?->nome_tabella ?? '')); } if ($name === '') { $name = trim((string) ($tabella?->nome_tabella_millesimale ?? '')); } return $this->formatTabellaLabel($code, $name, $tabella?->id); } private function formatTabellaLabel(?string $code, ?string $name, ?int $fallbackId = null): string { $code = trim((string) ($code ?? '')); $name = trim((string) ($name ?? '')); if ($code === '' && $fallbackId) { $code = 'TAB #' . (int) $fallbackId; } if ($code === '' && $name === '') { return 'NO TABELLA'; } $label = $code !== '' && $name !== '' ? ($code . ' — ' . $name) : ($name !== '' ? $name : $code); return mb_strtoupper($label, 'UTF-8'); } private function getNordOrderExpression(string $alias = 'tm'): string { $parts = []; if (Schema::hasColumn('tabelle_millesimali', 'nord')) { $parts[] = 'CAST(' . $alias . '.nord AS UNSIGNED)'; } if (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione')) { $parts[] = 'CAST(' . $alias . '.ordine_visualizzazione AS UNSIGNED)'; } if (Schema::hasColumn('tabelle_millesimali', 'ordinamento')) { $parts[] = 'CAST(' . $alias . '.ordinamento AS UNSIGNED)'; } if ($parts === []) { return '9999'; } return 'COALESCE(' . implode(', ', $parts) . ', 9999)'; } private function getNordValueForTabella(?TabellaMillesimale $tabella): ?int { if (! $tabella) { return null; } foreach (['nord', 'ordine_visualizzazione', 'ordinamento'] as $field) { if (isset($tabella->{$field}) && is_numeric($tabella->{$field})) { return (int) $tabella->{$field}; } } return null; } /** @return array */ private function getNordOptions(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return []; } $expr = $this->getNordOrderExpression('tabelle_millesimali'); if ($expr === '9999') { return []; } $q = TabellaMillesimale::query()->where('stabile_id', $stabileId); if (Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) { $anno = AnnoGestioneContext::resolveActiveAnno($user); $q->where('anno_gestione', $anno); } $values = $q->selectRaw('DISTINCT ' . $expr . ' as nord_val') ->pluck('nord_val') ->filter(fn($v) => $v !== null && $v !== '') ->unique() ->sort() ->values(); return $values->mapWithKeys(fn($v) => [(string) $v => (string) $v])->all(); } /** @return array */ private function getFirstTabellaCodesToHide(int $stabileId): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $tipoTab = $this->tipoGestioneTab ?: 'ordinaria'; $tipo = $this->normalizeTipoGestione($tipoTab); $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $hasAnno = Schema::hasColumn('tabelle_millesimali', 'anno_gestione'); $anno = AnnoGestioneContext::resolveActiveAnno($user); $q = TabellaMillesimale::query()->where('stabile_id', $stabileId); if ($hasAnno) { $q->where('anno_gestione', $anno); } if ($hasLegacyTipo) { $q->whereIn('tipo', $this->getLegacyTipoValues($tipo)); } $q->orderByRaw($this->getNordOrderExpression('tabelle_millesimali')) ->orderBy('codice_tabella'); return $q->limit(2)->pluck('codice_tabella')->filter()->values()->all(); } private function resolveTabellaForAnno(?TabellaMillesimale $tabella, int $stabileId): ?TabellaMillesimale { if (! $tabella) { return null; } if (! Schema::hasColumn('tabelle_millesimali', 'anno_gestione')) { return $tabella; } $user = Auth::user(); $anno = $user instanceof User ? AnnoGestioneContext::resolveActiveAnno($user) : null; if (! $anno) { return $tabella; } $codice = trim((string) ($tabella->codice_tabella ?? '')); if ($codice === '') { return $tabella; } $cacheKey = $stabileId . '|' . $anno . '|' . $codice; if (array_key_exists($cacheKey, $this->tabellaAnnoCache)) { return $this->tabellaAnnoCache[$cacheKey]; } $match = TabellaMillesimale::query() ->where('stabile_id', $stabileId) ->where('codice_tabella', $codice) ->where('anno_gestione', $anno) ->orderBy('ordine_visualizzazione') ->orderBy('ordinamento') ->orderBy('id') ->first(); $this->tabellaAnnoCache[$cacheKey] = $match ?: $tabella; return $this->tabellaAnnoCache[$cacheKey]; } /** * Applica il preset selezionato alle percentuali %Prop/%Inq delle voci attualmente in lista. */ public function applyPresetRipartizione(): void { if ($this->presetRipartizione === 'manuale') { return; } if ($this->presetRipartizione !== 'confedilizia') { return; } $records = $this->getTableQuery() ->get(['id', 'categoria']); foreach ($records as $record) { if (! $record instanceof VoceSpesa) { continue; } $categoria = $this->mapVoceCategoriaToConfedilizia((string) $record->categoria); $default = RipartizioneSpeseInquilini::getDefaultConfedilizia($categoria); $record->update([ 'percentuale_condomino' => (float) ($default['proprietario'] ?? 100), 'percentuale_inquilino' => (float) ($default['inquilino'] ?? 0), ]); } $this->dispatch('$refresh'); } 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 setTipoGestioneTab(string $tab): void { $tab = trim($tab); if (! in_array($tab, ['ordinaria', 'acqua', 'riscaldamento', 'straordinaria'], true)) { return; } $selectedYear = null; if ($this->gestioneContabileId && Schema::hasTable('gestioni_contabili')) { $year = GestioneContabile::query()->whereKey($this->gestioneContabileId)->value('anno_gestione'); $selectedYear = is_numeric($year) ? (int) $year : null; } $this->tipoGestioneTab = $tab; $user = Auth::user(); $stabileId = $user instanceof User ? StabileContext::resolveActiveStabileId($user) : null; if ($stabileId && $selectedYear) { $this->gestioneContabileId = $this->resolveGestioneIdForAnnoTipo( (int) $stabileId, $selectedYear, $this->normalizeTipoGestione($tab) ); } else { $this->gestioneContabileId = $this->resolveGestioneContabileId(); } // Filament v4: reset della paginazione tabella $this->resetPage(); } protected function resolveGestioneContabileId(): ?int { $user = Auth::user(); if (! $user instanceof User) { return null; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return null; } if (! Schema::hasTable('gestioni_contabili')) { return null; } $anno = AnnoGestioneContext::resolveActiveAnno($user); // Per questa pagina la gestione è determinata dalla tab selezionata. $tipo = $this->normalizeTipoGestione($this->tipoGestioneTab ?: 'ordinaria'); $match = GestioneContabile::query() ->where('stabile_id', $stabileId) ->where('anno_gestione', $anno) ->where('tipo_gestione', $tipo) ->where('stato', 'aperta') ->orderByDesc('gestione_attiva') ->orderByDesc('id') ->first(); return $match?->id ? (int) $match->id : null; } protected function getTableQuery(): Builder { $user = Auth::user(); if (! $user instanceof User) { return VoceSpesa::query()->whereRaw('1 = 0'); } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return VoceSpesa::query()->whereRaw('1 = 0'); } if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) { return VoceSpesa::query()->whereRaw('1 = 0'); } $tipoTab = $this->tipoGestioneTab; // Requisito (legacy): tabs filtrano per `tabelle.tipo` e ordinano per `tabelle.nord`. // Nel dominio corrente i nomi colonna possono variare: gestiamo fallback in modo robusto. $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $query = VoceSpesa::query() ->select('voci_spesa.*') ->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id') ->where('voci_spesa.stabile_id', $activeStabileId) ->when(Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId, function (Builder $q) { $q->where('voci_spesa.gestione_contabile_id', $this->gestioneContabileId); }) ->with('tabellaMillesimaleDefault'); if ($this->hideFirstTwoTabelle) { $hidden = $this->getFirstTabellaCodesToHide($activeStabileId); if (! empty($hidden)) { $query->where(function (Builder $q) use ($hidden) { $q->whereNull('tm.codice_tabella')->orWhereNotIn('tm.codice_tabella', $hidden); }); } } $query->orderByRaw($this->getNordOrderExpression('tm')); $query->orderBy('tm.codice_tabella'); if (Schema::hasColumn('voci_spesa', 'nord')) { $query->orderByRaw('COALESCE(voci_spesa.nord, voci_spesa.ordinamento, 9999)'); } else { $query->orderByRaw('COALESCE(voci_spesa.ordinamento, 9999)'); } $query->orderBy('voci_spesa.codice'); $query->orderBy('voci_spesa.descrizione'); $query->orderBy('voci_spesa.id'); $this->applyTipoGestioneFilter($query, $tipoTab, $hasLegacyTipo); return $query; } public function getTotalePreventivo(): float { $user = Auth::user(); if (! $user instanceof User) { return 0.0; } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return 0.0; } if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) { return 0.0; } $tipoTab = $this->tipoGestioneTab; $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $q = VoceSpesa::query()->where('voci_spesa.stabile_id', $activeStabileId); if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId) { $q->where('gestione_contabile_id', $this->gestioneContabileId); } $q->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id'); $this->applyTipoGestioneFilter($q, $tipoTab, $hasLegacyTipo); return (float) $q->sum('importo_default'); } public function getTotalePreventivoAcqua(): float { return $this->getTotaleByTipoTab('acqua', 'importo_default'); } public function getTotalePreventivoGenerale(): float { if (($this->tipoGestioneTab ?? 'ordinaria') === 'acqua') { return $this->getTotalePreventivo(); } return $this->getTotalePreventivo() + $this->getTotalePreventivoAcqua(); } public function getTotaleConsuntivo(): float { $user = Auth::user(); if (! $user instanceof User) { return 0.0; } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return 0.0; } if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) { return 0.0; } if (! Schema::hasColumn('voci_spesa', 'importo_consuntivo')) { return 0.0; } $tipoTab = $this->tipoGestioneTab; $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $q = VoceSpesa::query()->where('voci_spesa.stabile_id', $activeStabileId); if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId) { $q->where('gestione_contabile_id', $this->gestioneContabileId); } $q->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id'); $this->applyTipoGestioneFilter($q, $tipoTab, $hasLegacyTipo); return (float) $q->sum('importo_consuntivo'); } public function getTotaleConsuntivoAcqua(): float { return $this->getTotaleByTipoTab('acqua', 'importo_consuntivo'); } public function getTotaleConsuntivoGenerale(): float { if (($this->tipoGestioneTab ?? 'ordinaria') === 'acqua') { return $this->getTotaleConsuntivo(); } return $this->getTotaleConsuntivo() + $this->getTotaleConsuntivoAcqua(); } private function getTotaleByTipoTab(string $tipoTab, string $column): float { $user = Auth::user(); if (! $user instanceof User) { return 0.0; } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return 0.0; } if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) { return 0.0; } if (! Schema::hasColumn('voci_spesa', $column)) { return 0.0; } $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $q = VoceSpesa::query()->where('voci_spesa.stabile_id', $activeStabileId); if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId) { $q->where('gestione_contabile_id', $this->gestioneContabileId); } $q->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id'); $this->applyTipoGestioneFilter($q, $tipoTab, $hasLegacyTipo); return (float) $q->sum($column); } /** * Totali per tabella (codice_tabella) e per Prev/Cons. * @return array */ public function getTotaliPerTabella(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $activeStabileId = StabileContext::resolveActiveStabileId($user); if (! $activeStabileId) { return []; } if (Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && ! $this->gestioneContabileId) { return []; } $tipoTab = $this->tipoGestioneTab; $hasLegacyTipo = Schema::hasColumn('tabelle_millesimali', 'tipo'); $q = VoceSpesa::query() ->leftJoin('tabelle_millesimali as tm', 'tm.id', '=', 'voci_spesa.tabella_millesimale_default_id') ->where('voci_spesa.stabile_id', $activeStabileId) ->when(Schema::hasColumn('voci_spesa', 'gestione_contabile_id') && $this->gestioneContabileId, function (Builder $qq) { $qq->where('voci_spesa.gestione_contabile_id', $this->gestioneContabileId); }); $q->selectRaw('COALESCE(tm.codice_tabella, "—") as tabella, COALESCE(tm.nord, tm.ordine_visualizzazione, tm.ordinamento, 9999) as tabella_nord, SUM(voci_spesa.importo_default) as totale_prev, SUM(COALESCE(voci_spesa.importo_consuntivo, 0)) as totale_cons'); $this->applyTipoGestioneFilter($q, $tipoTab, $hasLegacyTipo); return $q ->groupBy('tabella', 'tabella_nord') ->orderBy('tabella_nord') ->orderByRaw('tabella = "—"') ->orderBy('tabella') ->get() ->map(fn($r) => [ 'tabella' => (string) $r->tabella, 'totale_prev' => (float) $r->totale_prev, 'totale_cons' => (float) $r->totale_cons, ]) ->all(); } public function table(Table $table): Table { return $table ->striped() ->groups([ Group::make('tabellaMillesimaleDefault.codice_tabella') ->label('Tabella') ->titlePrefixedWithLabel(false) ->getTitleFromRecordUsing(fn(VoceSpesa $record): string => $this->getTabellaGroupTitle($record)), ]) ->defaultGroup('tabellaMillesimaleDefault.codice_tabella') ->groupingSettingsHidden() ->filters([ SelectFilter::make('tabella') ->label('Tabella') ->options(fn(): array=> $this->getTabelleMillesimaliOptions()) ->searchable() ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if (! is_numeric($value)) { return $query; } return $query->where('voci_spesa.tabella_millesimale_default_id', (int) $value); }), SelectFilter::make('nord') ->label('NORD') ->options(fn(): array=> $this->getNordOptions()) ->query(function (Builder $query, array $data): Builder { $value = $data['value'] ?? null; if ($value === null || $value === '') { return $query; } return $query->whereRaw($this->getNordOrderExpression('tm') . ' = ?', [$value]); }), ]) ->columns([ TextColumn::make('descrizione') ->label('Descrizione') ->searchable() ->wrap() ->extraAttributes(['class' => 'text-[13px] leading-tight']), TextColumn::make('importo_default') ->label('Prev.') ->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.')) ->alignRight() ->summarize(Sum::make()->label('Subtotale')->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.'))) ->extraAttributes(['class' => 'text-[13px] leading-tight']), TextColumn::make('importo_consuntivo') ->label('Cons.') ->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.')) ->alignRight() ->summarize(Sum::make()->label('Subtotale')->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.'))) ->extraAttributes(['class' => 'text-[13px] leading-tight']), TextColumn::make('percentuale_condomino') ->label('% Prop.') ->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.')) ->alignRight() ->extraAttributes(['class' => 'text-[13px] leading-tight']), TextColumn::make('percentuale_inquilino') ->label('% Inq.') ->formatStateUsing(fn($state) => number_format((float) ($state ?? 0), 2, ',', '.')) ->alignRight() ->extraAttributes(['class' => 'text-[13px] leading-tight']), IconColumn::make('attiva') ->label('Attiva') ->boolean() ->extraAttributes(['class' => 'text-[13px] leading-tight']), ]) ->actions([ Action::make('mastrino') ->hiddenLabel() ->icon('heroicon-o-document-chart-bar') ->tooltip('Mastrino') ->url(fn(VoceSpesa $record) => VoceSpesaMastrino::getUrl([ 'record' => (int) $record->id, 'gestione' => $this->gestioneContabileId, ], panel: 'admin-filament')), Action::make('modifica') ->hiddenLabel() ->icon('heroicon-o-pencil-square') ->tooltip('Modifica') ->modalHeading('Modifica voce di spesa') ->form([ Select::make('tabella_millesimale_default_id') ->label('Tabella') ->options(fn() => $this->getTabelleMillesimaliOptions()) ->searchable() ->placeholder('— Seleziona —') ->native(false), Select::make('attiva') ->label('Attiva') ->options([1 => 'Sì', 0 => 'No']) ->default(1) ->native(false), Select::make('categoria') ->label('Categoria') ->options(VoceSpesa::getCategorieStandard()) ->searchable() ->native(false), Select::make('tipo_gestione') ->label('Tipo gestione') ->options(VoceSpesa::getTipiGestione()) ->native(false), \Filament\Forms\Components\TextInput::make('importo_default') ->label('Importo preventivo') ->numeric() ->step('0.01'), \Filament\Forms\Components\TextInput::make('importo_consuntivo') ->label('Importo consuntivo') ->numeric() ->step('0.01'), \Filament\Forms\Components\TextInput::make('percentuale_condomino') ->label('% Proprietario') ->numeric() ->step('0.01') ->minValue(0) ->maxValue(100), \Filament\Forms\Components\TextInput::make('percentuale_inquilino') ->label('% Inquilino') ->numeric() ->step('0.01') ->minValue(0) ->maxValue(100), ]) ->fillForm(function (VoceSpesa $record): array { return [ 'tabella_millesimale_default_id' => $record->tabella_millesimale_default_id, 'attiva' => $record->attiva ? 1 : 0, 'categoria' => $record->categoria, 'tipo_gestione' => $record->tipo_gestione, 'importo_default' => $record->importo_default, 'importo_consuntivo' => $record->importo_consuntivo, 'percentuale_condomino' => $record->percentuale_condomino, 'percentuale_inquilino' => $record->percentuale_inquilino, ]; }) ->action(function (array $data, VoceSpesa $record): void { $cond = isset($data['percentuale_condomino']) ? (float) $data['percentuale_condomino'] : null; $inq = isset($data['percentuale_inquilino']) ? (float) $data['percentuale_inquilino'] : null; if ($cond !== null && $inq === null) { $inq = max(0.0, 100.0 - $cond); } $record->update([ 'tabella_millesimale_default_id' => is_numeric($data['tabella_millesimale_default_id'] ?? null) ? (int) $data['tabella_millesimale_default_id'] : null, 'attiva' => (bool) ($data['attiva'] ?? true), 'categoria' => $data['categoria'] ?? $record->categoria, 'tipo_gestione' => $data['tipo_gestione'] ?? $record->tipo_gestione, 'importo_default' => $data['importo_default'] ?? $record->importo_default, 'importo_consuntivo' => $data['importo_consuntivo'] ?? $record->importo_consuntivo, 'percentuale_condomino' => $cond ?? $record->percentuale_condomino, 'percentuale_inquilino' => $inq ?? $record->percentuale_inquilino, ]); $this->dispatch('$refresh'); }), Action::make('assegnaTabella') ->hiddenLabel() ->icon('heroicon-o-rectangle-stack') ->tooltip('Tabella') ->modalHeading('Imposta tabella di riferimento') ->form([ Select::make('tabella_millesimale_default_id') ->label('Tabella') ->options(fn() => $this->getTabelleMillesimaliOptions()) ->searchable() ->placeholder('— Seleziona —') ->native(false), ]) ->fillForm(function (VoceSpesa $record): array { return [ 'tabella_millesimale_default_id' => $record->tabella_millesimale_default_id, ]; }) ->action(function (array $data, VoceSpesa $record): void { $id = is_numeric($data['tabella_millesimale_default_id'] ?? null) ? (int) $data['tabella_millesimale_default_id'] : null; $record->update(['tabella_millesimale_default_id' => $id && $id > 0 ? $id : null]); $this->dispatch('$refresh'); }), ]) ->paginationPageOptions([24, 48, 96, 150]) ->defaultPaginationPageOption(48) ->bulkActions([ BulkAction::make('bulkAssegnaTabella') ->label('Assegna tabella') ->icon('heroicon-o-wrench-screwdriver') ->modalHeading('Assegna tabella alle voci selezionate') ->form([ Select::make('tabella_millesimale_default_id') ->label('Tabella') ->options(fn() => $this->getTabelleMillesimaliOptions()) ->searchable() ->required() ->native(false), ]) ->action(function (array $data, $records): void { $id = is_numeric($data['tabella_millesimale_default_id'] ?? null) ? (int) $data['tabella_millesimale_default_id'] : null; if (! $id || $id <= 0) { return; } foreach ($records as $record) { if ($record instanceof VoceSpesa) { $record->update(['tabella_millesimale_default_id' => $id]); } } $this->tabellaFocusId = $this->tabellaFocusId ?: $id; $this->dispatch('$refresh'); }), BulkAction::make('bulkRimuoviTabella') ->label('Rimuovi tabella') ->icon('heroicon-o-x-mark') ->requiresConfirmation() ->action(function ($records): void { foreach ($records as $record) { if ($record instanceof VoceSpesa) { $record->update(['tabella_millesimale_default_id' => null]); } } $this->dispatch('$refresh'); }), ]); } }