netgescon-day0/app/Filament/Pages/Contabilita/VociSpesaArchivio.php

1388 lines
52 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Filament\Pages\Contabilita;
use App\Filament\Pages\Condomini\TabelleMillesimaliProspetto;
use App\Filament\Pages\Contabilita\VoceSpesaMastrino;
use App\Models\RipartizioneSpeseInquilini;
use App\Models\TabellaMillesimale;
use App\Models\User;
use App\Models\VoceSpesa;
use App\Models\GestioneContabile;
use App\Support\StabileContext;
use App\Support\AnnoGestioneContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\BulkAction;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Toggle;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\Summarizers\Sum;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Grouping\Group;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use UnitEnum;
class VociSpesaArchivio extends Page implements HasTable
{
use InteractsWithTable;
protected static ?string $navigationLabel = 'Voci di spesa';
protected static ?string $title = 'Voci di spesa';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static UnitEnum|string|null $navigationGroup = 'Contabilità';
protected static ?int $navigationSort = 40;
protected static ?string $slug = 'contabilita/voci-spesa';
protected string $view = 'filament.pages.contabilita.voci-spesa-prospetto';
public string $tipoGestioneTab = 'ordinaria';
public ?int $gestioneContabileId = null;
public string $presetRipartizione = 'manuale';
public ?int $tabellaFocusId = null;
public bool $hideFirstTwoTabelle = false;
/** @var array<string, TabellaMillesimale|null> */
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 allanno 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<int, string> */
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<int, array{codice: string, descrizione: string, prev: float, cons: float, prop: float, inq: float, attiva: bool}>} */
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<int, string> */
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<int, string> */
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<int, string> */
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<int, array{tabella: string, totale_prev: float, totale_cons: float}>
*/
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');
}),
]);
}
}