netgescon-day0/app/Filament/Pages/Condomini/TabelleMillesimaliArchivio.php

1371 lines
52 KiB
PHP

<?php
namespace App\Filament\Pages\Condomini;
use App\Models\DettaglioMillesimi;
use App\Models\Stabile;
use App\Models\TabellaMillesimale;
use App\Models\UnitaImmobiliare;
use App\Models\User;
use App\Models\VoceSpesa;
use App\Models\RipartizioneSpeseInquilini;
use App\Models\RipartizionePreset;
use App\Support\AnnoGestioneContext;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Notifications\Notification;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\Toggle;
use Filament\Pages\Page;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
use UnitEnum;
class TabelleMillesimaliArchivio extends Page implements HasTable
{
use InteractsWithTable;
protected static ?string $navigationLabel = 'Tabelle millesimali';
protected static ?string $title = 'Tabelle millesimali';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-table-cells';
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
protected static ?int $navigationSort = 32;
protected static ?string $slug = 'condomini/tabelle-millesimali';
protected string $view = 'filament.pages.condomini.tabelle-millesimali';
protected int $stabileId = 0;
protected int $annoGestione = 0;
public string $tab = 'archivio';
public ?int $tabellaId = null;
public ?string $codiceStabile = null;
/** @var array<int, array<string, mixed>> */
public array $vociSpesa = [];
public string $presetRipartizione = 'manuale';
/** @var array<int, string> */
public array $presetOptions = [];
public ?int $presetId = null;
public string $presetNome = '';
public bool $presetDirty = false;
/** @var array<int, array<string, mixed>> */
public array $tabelle = [];
/** @var array<int, array<string, mixed>> */
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', 'nord')
? 'nord'
: (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : (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', 'nord')
? 'nord'
: (Schema::hasColumn('tabelle_millesimali', 'ordine_visualizzazione') ? 'ordine_visualizzazione' : 'ordinamento')
)
->label('Nord')
->sortable()
->toggleable(),
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', 'ordine_visualizzazione', 'nord', '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,
'nord' => $this->resolveNordValue($t),
'tipo' => $tipoLabel,
'calcolo' => $calcoloLabel,
'is_consumo' => $isConsumo,
'totale' => is_numeric($t->totale_millesimi) ? (float) $t->totale_millesimi : null,
'ordinamento' => $this->resolveNordValue($t) ?? (int) ($t->ordine_visualizzazione ?? $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();
usort($this->tabelle, static function (array $left, array $right): int {
$leftNord = is_numeric($left['ordinamento'] ?? null) ? (int) $left['ordinamento'] : 999999;
$rightNord = is_numeric($right['ordinamento'] ?? null) ? (int) $right['ordinamento'] : 999999;
if ($leftNord !== $rightNord) {
return $leftNord <=> $rightNord;
}
$leftTipo = (string) ($left['tipo'] ?? '');
$rightTipo = (string) ($right['tipo'] ?? '');
if ($leftTipo !== $rightTipo) {
return strnatcasecmp($leftTipo, $rightTipo);
}
return strnatcasecmp((string) ($left['codice'] ?? ''), (string) ($right['codice'] ?? ''));
});
}
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;
}
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<int, array<string, mixed>> $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);
}
private function resolveNordValue(TabellaMillesimale $t): ?int
{
foreach (['nord', 'ordine_visualizzazione', 'ordinamento'] as $field) {
$value = $t->{$field} ?? null;
if (is_numeric($value)) {
return (int) $value;
}
}
return null;
}
}