1760 lines
69 KiB
PHP
Executable File
1760 lines
69 KiB
PHP
Executable File
<?php
|
|
namespace App\Filament\Pages\Condomini;
|
|
|
|
use App\Models\DettaglioMillesimi;
|
|
use App\Models\RipartizionePreset;
|
|
use App\Models\RipartizioneSpeseInquilini;
|
|
use App\Models\Stabile;
|
|
use App\Models\TabellaMillesimale;
|
|
use App\Models\UnitaImmobiliare;
|
|
use App\Models\User;
|
|
use App\Models\VoceSpesa;
|
|
use App\Support\AnnoGestioneContext;
|
|
use App\Support\StabileContext;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Forms\Components\TextInput;
|
|
use Filament\Forms\Components\Toggle;
|
|
use Filament\Notifications\Notification;
|
|
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\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Illuminate\Support\Facades\Validator;
|
|
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, int> */
|
|
public array $selectedArchiveIds = [];
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
public array $righe = [];
|
|
|
|
public ?array $tabellaInfo = null;
|
|
|
|
/** @var array<int, array<string, mixed>> */
|
|
protected array $archiveDeletionAnalysisCache = [];
|
|
|
|
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('nord')
|
|
->label('NORD')
|
|
->numeric()
|
|
->rules(['nullable', 'integer', 'min:0'])
|
|
->helperText('Ordinamento legacy (es. 10 per TAB.A, 20 per TAB.A1).'),
|
|
|
|
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,
|
|
'nord' => is_numeric($data['nord'] ?? null) ? (int) $data['nord'] : 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);
|
|
$this->resetArchiveDeletionAnalysisCache();
|
|
$this->loadTabelle();
|
|
$this->tabellaId = $this->pickTabellaId($this->tabellaId);
|
|
$this->loadDettaglioTabella();
|
|
$this->loadVociSpesa();
|
|
}),
|
|
|
|
Action::make('delete_selected_archives')
|
|
->label(fn(): string => 'Elimina selezionate (' . count($this->normalizeSelectedArchiveIds()) . ')')
|
|
->icon('heroicon-o-trash')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->modalHeading('Elimina archivi millesimali selezionati')
|
|
->modalDescription('Saranno eliminate solo le tabelle senza collegamenti attivi. Le tabelle ancora usate da dettagli, voci o altri archivi verranno saltate.')
|
|
->disabled(fn(): bool => $this->normalizeSelectedArchiveIds() === [])
|
|
->action(fn(): mixed => $this->deleteSelectedArchives()),
|
|
|
|
Action::make('select_without_nord')
|
|
->label('Seleziona senza NORD')
|
|
->icon('heroicon-o-funnel')
|
|
->color('gray')
|
|
->action(fn(): mixed => $this->selectArchivesWithoutNord()),
|
|
|
|
Action::make('delete_selected_archives_with_voices')
|
|
->label('Elimina selezionate + voci')
|
|
->icon('heroicon-o-trash')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->modalHeading('Pulisci tabelle senza NORD e voci collegate')
|
|
->modalDescription('Elimina le tabelle selezionate e, se richiesto, cancella anche le voci collegate direttamente a quelle tabelle. Restano bloccate le tabelle con altri riferimenti attivi.')
|
|
->disabled(fn(): bool => $this->normalizeSelectedArchiveIds() === [])
|
|
->action(fn(): mixed => $this->deleteSelectedArchives(true)),
|
|
])
|
|
->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('cleanup_status')
|
|
->label('Integrità')
|
|
->getStateUsing(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupBadge($record))
|
|
->tooltip(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupTooltip($record))
|
|
->badge()
|
|
->color(fn(TabellaMillesimale $record): string => $this->analyzeArchiveDeletion($record)['deletable'] ? 'success' : 'warning')
|
|
->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('select_cleanup')
|
|
->label(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'Desel.' : 'Sel.')
|
|
->icon(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'heroicon-o-check-circle' : 'heroicon-o-plus-circle')
|
|
->color(fn(TabellaMillesimale $record): string => in_array((int) $record->id, $this->normalizeSelectedArchiveIds(), true) ? 'primary' : 'gray')
|
|
->tooltip(fn(TabellaMillesimale $record): string => $this->getArchiveCleanupTooltip($record))
|
|
->action(fn(TabellaMillesimale $record): mixed => $this->toggleArchiveSelection((int) $record->id))
|
|
->iconButton(),
|
|
|
|
Action::make('edit')
|
|
->label('Modifica')
|
|
->icon('heroicon-o-pencil-square')
|
|
->modalHeading('Modifica tabella millesimale')
|
|
->iconButton()
|
|
->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('nord')
|
|
->label('NORD')
|
|
->numeric()
|
|
->rules(['nullable', 'integer', 'min:0'])
|
|
->helperText('Ordinamento legacy (es. 10 per TAB.A, 20 per TAB.A1).'),
|
|
|
|
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,
|
|
'nord' => $record->nord,
|
|
'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,
|
|
'nord' => is_numeric($data['nord'] ?? null) ? (int) $data['nord'] : 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);
|
|
$this->resetArchiveDeletionAnalysisCache();
|
|
}),
|
|
|
|
Action::make('delete')
|
|
->label('Elimina')
|
|
->icon('heroicon-o-trash')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->action(fn(TabellaMillesimale $record): mixed => $this->deleteSingleArchive($record))
|
|
->iconButton(),
|
|
|
|
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)
|
|
->iconButton(),
|
|
]);
|
|
}
|
|
|
|
protected function normalizeSelectedArchiveIds(): array
|
|
{
|
|
$selected = array_values(array_unique(array_map(
|
|
static fn($id): int => (int) $id,
|
|
array_filter($this->selectedArchiveIds, static fn($id): bool => is_numeric($id) && (int) $id > 0)
|
|
)));
|
|
|
|
$this->selectedArchiveIds = $selected;
|
|
|
|
return $selected;
|
|
}
|
|
|
|
public function toggleArchiveSelection(int $tabellaId): void
|
|
{
|
|
$selected = $this->normalizeSelectedArchiveIds();
|
|
|
|
if (in_array($tabellaId, $selected, true)) {
|
|
$this->selectedArchiveIds = array_values(array_filter($selected, static fn(int $id): bool => $id !== $tabellaId));
|
|
return;
|
|
}
|
|
|
|
$selected[] = $tabellaId;
|
|
$this->selectedArchiveIds = array_values(array_unique($selected));
|
|
}
|
|
|
|
protected function deleteSelectedArchives(bool $cascadeLinkedVoices = false): void
|
|
{
|
|
$selected = $this->normalizeSelectedArchiveIds();
|
|
if ($selected === []) {
|
|
Notification::make()
|
|
->title('Nessuna tabella selezionata')
|
|
->warning()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$records = TabellaMillesimale::query()
|
|
->where('stabile_id', $this->stabileId)
|
|
->whereIn('id', $selected)
|
|
->get();
|
|
|
|
$deleted = 0;
|
|
$blocked = [];
|
|
$deletedVoices = 0;
|
|
|
|
foreach ($records as $record) {
|
|
$analysis = $this->analyzeArchiveDeletion($record, $cascadeLinkedVoices);
|
|
if (! $analysis['deletable']) {
|
|
$blocked[] = sprintf('%s (%s)', $record->codice_tabella ?: ('Tabella #' . $record->id), $analysis['summary']);
|
|
continue;
|
|
}
|
|
|
|
$deletedVoices += $this->purgeArchive($record, $cascadeLinkedVoices);
|
|
$deleted++;
|
|
}
|
|
|
|
$this->selectedArchiveIds = array_values(array_diff($selected, $records->pluck('id')->map(fn($id): int => (int) $id)->all()));
|
|
$this->resetArchiveDeletionAnalysisCache();
|
|
$this->loadTabelle();
|
|
$this->tabellaId = $this->pickTabellaId($this->tabellaId && ! in_array($this->tabellaId, $selected, true) ? $this->tabellaId : null);
|
|
$this->loadDettaglioTabella();
|
|
$this->loadVociSpesa();
|
|
|
|
$notification = Notification::make();
|
|
|
|
if ($deleted > 0) {
|
|
$notification->title('Pulizia archivi completata')->success();
|
|
} else {
|
|
$notification->title('Nessun archivio eliminato')->warning();
|
|
}
|
|
|
|
$body = [];
|
|
if ($deleted > 0) {
|
|
$body[] = $deleted . ' tabella/e eliminata/e.';
|
|
}
|
|
if ($deletedVoices > 0) {
|
|
$body[] = $deletedVoices . ' voce/i collegate eliminate.';
|
|
}
|
|
if ($blocked !== []) {
|
|
$body[] = 'Saltate per collegamenti attivi: ' . implode('; ', array_slice($blocked, 0, 5));
|
|
if (count($blocked) > 5) {
|
|
$body[] = 'Altre tabelle bloccate: ' . (count($blocked) - 5) . '.';
|
|
}
|
|
}
|
|
|
|
if ($body !== []) {
|
|
$notification->body(implode(' ', $body));
|
|
}
|
|
|
|
$notification->send();
|
|
}
|
|
|
|
protected function deleteSingleArchive(TabellaMillesimale $record): void
|
|
{
|
|
$analysis = $this->analyzeArchiveDeletion($record);
|
|
if (! $analysis['deletable']) {
|
|
Notification::make()
|
|
->title('Eliminazione bloccata')
|
|
->body('La tabella e ancora collegata: ' . $analysis['summary'])
|
|
->warning()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$this->purgeArchive($record);
|
|
$this->selectedArchiveIds = array_values(array_filter($this->normalizeSelectedArchiveIds(), static fn(int $id): bool => $id !== (int) $record->id));
|
|
$this->resetArchiveDeletionAnalysisCache();
|
|
$this->loadTabelle();
|
|
$this->tabellaId = $this->pickTabellaId($this->tabellaId === (int) $record->id ? null : $this->tabellaId);
|
|
$this->loadDettaglioTabella();
|
|
$this->loadVociSpesa();
|
|
|
|
Notification::make()
|
|
->title('Tabella eliminata')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
protected function analyzeArchiveDeletion(TabellaMillesimale $record, bool $cascadeLinkedVoices = false): array
|
|
{
|
|
$recordId = (int) $record->id;
|
|
$cacheKey = $recordId . '|' . ($cascadeLinkedVoices ? 'cascade' : 'default');
|
|
|
|
if (isset($this->archiveDeletionAnalysisCache[$cacheKey])) {
|
|
return $this->archiveDeletionAnalysisCache[$cacheKey];
|
|
}
|
|
|
|
$sources = [
|
|
'dettaglio_millesimi' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli millesimi'],
|
|
'dettagli_tabelle_millesimali' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli tabelle'],
|
|
'dettagli_millesimali' => ['fk' => 'tabella_id', 'label' => 'dettagli legacy'],
|
|
'voci_spesa' => ['fk' => 'tabella_millesimale_default_id', 'label' => 'voci spesa'],
|
|
'voci_preventivo' => ['fk' => 'tabella_millesimale_id', 'label' => 'voci preventivo'],
|
|
'ripartizione_spese' => ['fk' => 'tabella_millesimale_id', 'label' => 'ripartizioni'],
|
|
'ordine_giorno' => ['fk' => 'tabella_millesimale_id', 'label' => 'ordini del giorno'],
|
|
'dettaglio_movimenti' => ['fk' => 'tabella_millesimale_id', 'label' => 'dettagli movimenti'],
|
|
];
|
|
|
|
$counts = [];
|
|
foreach ($sources as $table => $config) {
|
|
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, $config['fk'])) {
|
|
continue;
|
|
}
|
|
|
|
$count = DB::table($table)
|
|
->where($config['fk'], $recordId)
|
|
->count();
|
|
|
|
if ($count > 0) {
|
|
$counts[$config['label']] = $count;
|
|
}
|
|
}
|
|
|
|
$blockingCounts = $counts;
|
|
if ($cascadeLinkedVoices) {
|
|
unset($blockingCounts['voci spesa'], $blockingCounts['voci preventivo']);
|
|
}
|
|
|
|
$summaryParts = [];
|
|
foreach ($counts as $label => $count) {
|
|
$summaryParts[] = $label . ': ' . $count;
|
|
}
|
|
|
|
return $this->archiveDeletionAnalysisCache[$cacheKey] = [
|
|
'deletable' => $blockingCounts === [],
|
|
'total_links' => array_sum($counts),
|
|
'counts' => $counts,
|
|
'summary' => $summaryParts === [] ? 'nessun collegamento attivo' : implode(', ', $summaryParts),
|
|
];
|
|
}
|
|
|
|
protected function getArchiveCleanupBadge(TabellaMillesimale $record): string
|
|
{
|
|
$analysis = $this->analyzeArchiveDeletion($record);
|
|
|
|
if ($analysis['deletable']) {
|
|
return 'Orfana';
|
|
}
|
|
|
|
return $analysis['total_links'] . ' colleg.';
|
|
}
|
|
|
|
protected function getArchiveCleanupTooltip(TabellaMillesimale $record): string
|
|
{
|
|
$analysis = $this->analyzeArchiveDeletion($record);
|
|
|
|
if ($analysis['deletable']) {
|
|
return 'Tabella non collegata: eliminabile in pulizia.';
|
|
}
|
|
|
|
return 'Tabella collegata, pulizia bloccata: ' . $analysis['summary'];
|
|
}
|
|
|
|
protected function resetArchiveDeletionAnalysisCache(): void
|
|
{
|
|
$this->archiveDeletionAnalysisCache = [];
|
|
}
|
|
|
|
protected function selectArchivesWithoutNord(): void
|
|
{
|
|
if (! Schema::hasColumn('tabelle_millesimali', 'nord')) {
|
|
Notification::make()
|
|
->title('Campo NORD non disponibile')
|
|
->warning()
|
|
->send();
|
|
return;
|
|
}
|
|
|
|
$ids = TabellaMillesimale::query()
|
|
->where('stabile_id', $this->stabileId)
|
|
->where(function (Builder $query): void {
|
|
$query->whereNull('nord')
|
|
->orWhere('nord', '');
|
|
})
|
|
->pluck('id')
|
|
->map(fn($id): int => (int) $id)
|
|
->filter(fn(int $id): bool => $id > 0)
|
|
->values()
|
|
->all();
|
|
|
|
$this->selectedArchiveIds = $ids;
|
|
|
|
Notification::make()
|
|
->title('Selezione aggiornata')
|
|
->body(count($ids) . ' tabella/e senza NORD selezionata/e.')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
protected function deleteLinkedVoicesForArchive(TabellaMillesimale $record): int
|
|
{
|
|
$deleted = 0;
|
|
|
|
if (Schema::hasTable('voci_spesa') && Schema::hasColumn('voci_spesa', 'tabella_millesimale_default_id')) {
|
|
$voiceIds = VoceSpesa::query()
|
|
->where('stabile_id', $this->stabileId)
|
|
->where('tabella_millesimale_default_id', (int) $record->id)
|
|
->pluck('id')
|
|
->map(fn($id): int => (int) $id)
|
|
->all();
|
|
|
|
if ($voiceIds !== []) {
|
|
$deleted += VoceSpesa::query()->whereIn('id', $voiceIds)->delete();
|
|
}
|
|
}
|
|
|
|
if (Schema::hasTable('voci_preventivo') && Schema::hasColumn('voci_preventivo', 'tabella_millesimale_id')) {
|
|
$deleted += DB::table('voci_preventivo')
|
|
->where('tabella_millesimale_id', (int) $record->id)
|
|
->delete();
|
|
}
|
|
|
|
return $deleted;
|
|
}
|
|
|
|
protected function purgeArchive(TabellaMillesimale $record, bool $cascadeLinkedVoices = false): int
|
|
{
|
|
return (int) DB::transaction(function () use ($record, $cascadeLinkedVoices): int {
|
|
$deletedVoices = 0;
|
|
|
|
if ($cascadeLinkedVoices) {
|
|
$deletedVoices = $this->deleteLinkedVoicesForArchive($record);
|
|
}
|
|
|
|
$record->forceDelete();
|
|
|
|
return $deletedVoices;
|
|
});
|
|
}
|
|
|
|
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;
|
|
|
|
$vociCount = 0;
|
|
if (Schema::hasTable('voci_spesa')) {
|
|
$vociCount = DB::table('voci_spesa')
|
|
->where('stabile_id', $this->stabileId)
|
|
->where('tabella_millesimale_default_id', $t->id)
|
|
->count();
|
|
}
|
|
|
|
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,
|
|
'voci_count' => $vociCount,
|
|
];
|
|
})
|
|
->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 [
|
|
'id' => (int) $d->id,
|
|
'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),
|
|
'nord' => $d->nord,
|
|
'ruolo_legacy' => $d->ruolo_legacy,
|
|
];
|
|
})
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
protected function loadVociSpesa(): void
|
|
{
|
|
$this->vociSpesa = [];
|
|
|
|
if (! $this->stabileId || ! $this->tabellaId) {
|
|
return;
|
|
}
|
|
|
|
if (! Schema::hasTable('voci_spesa')) {
|
|
return;
|
|
}
|
|
|
|
$hasCond = Schema::hasColumn('voci_spesa', 'percentuale_condomino');
|
|
$hasInq = Schema::hasColumn('voci_spesa', 'percentuale_inquilino');
|
|
|
|
$rows = VoceSpesa::query()
|
|
->where('stabile_id', $this->stabileId)
|
|
->where('tabella_millesimale_default_id', $this->tabellaId)
|
|
->orderBy('ordinamento')
|
|
->orderBy('descrizione')
|
|
->get();
|
|
|
|
$this->vociSpesa = $rows->map(function (VoceSpesa $v) use ($hasCond, $hasInq): array {
|
|
return [
|
|
'id' => (int) $v->id,
|
|
'codice' => $v->codice,
|
|
'descrizione' => $v->descrizione,
|
|
'categoria' => $v->categoria,
|
|
'tipo_gestione' => $v->tipo_gestione,
|
|
'percentuale_condomino' => $hasCond ? (float) ($v->percentuale_condomino ?? 0) : null,
|
|
'percentuale_inquilino' => $hasInq ? (float) ($v->percentuale_inquilino ?? 0) : null,
|
|
'attiva' => (bool) ($v->attiva ?? true),
|
|
'modifica' => false,
|
|
];
|
|
})->all();
|
|
|
|
$this->presetDirty = false;
|
|
}
|
|
|
|
protected function loadPresetOptions(): void
|
|
{
|
|
$this->presetOptions = [];
|
|
|
|
if (! $this->stabileId) {
|
|
return;
|
|
}
|
|
|
|
if (! Schema::hasTable('ripartizione_presets')) {
|
|
return;
|
|
}
|
|
|
|
$this->presetOptions = RipartizionePreset::query()
|
|
->where('stabile_id', $this->stabileId)
|
|
->orderBy('nome')
|
|
->pluck('nome', 'id')
|
|
->map(fn($v) => (string) $v)
|
|
->all();
|
|
}
|
|
|
|
public function applyPresetRipartizione(): void
|
|
{
|
|
if ($this->presetRipartizione === 'manuale') {
|
|
return;
|
|
}
|
|
|
|
if ($this->presetRipartizione !== 'confedilizia') {
|
|
return;
|
|
}
|
|
|
|
foreach ($this->vociSpesa as $i => $voce) {
|
|
$categoria = $this->mapVoceCategoriaToConfedilizia((string) ($voce['categoria'] ?? ''));
|
|
$default = RipartizioneSpeseInquilini::getDefaultConfedilizia($categoria);
|
|
|
|
$this->vociSpesa[$i]['percentuale_condomino'] = (float) ($default['proprietario'] ?? 100);
|
|
$this->vociSpesa[$i]['percentuale_inquilino'] = (float) ($default['inquilino'] ?? 0);
|
|
$this->vociSpesa[$i]['modifica'] = true;
|
|
}
|
|
|
|
$this->presetDirty = true;
|
|
}
|
|
|
|
public function applyCustomPreset(): void
|
|
{
|
|
if (! $this->presetId || ! Schema::hasTable('ripartizione_presets')) {
|
|
return;
|
|
}
|
|
|
|
$preset = RipartizionePreset::query()
|
|
->where('stabile_id', $this->stabileId)
|
|
->whereKey($this->presetId)
|
|
->first();
|
|
|
|
if (! $preset) {
|
|
return;
|
|
}
|
|
|
|
$data = is_array($preset->dati) ? $preset->dati : [];
|
|
$map = [];
|
|
foreach ($data as $row) {
|
|
$codice = isset($row['codice']) ? (string) $row['codice'] : '';
|
|
if ($codice !== '') {
|
|
$map[$codice] = $row;
|
|
}
|
|
}
|
|
|
|
foreach ($this->vociSpesa as $i => $voce) {
|
|
$codice = (string) ($voce['codice'] ?? '');
|
|
if ($codice === '' || ! isset($map[$codice])) {
|
|
continue;
|
|
}
|
|
|
|
$row = $map[$codice];
|
|
$cond = isset($row['percentuale_condomino']) && is_numeric($row['percentuale_condomino'])
|
|
? (float) $row['percentuale_condomino']
|
|
: 100.0;
|
|
$inq = isset($row['percentuale_inquilino']) && is_numeric($row['percentuale_inquilino'])
|
|
? (float) $row['percentuale_inquilino']
|
|
: 0.0;
|
|
|
|
$this->vociSpesa[$i]['percentuale_condomino'] = $cond;
|
|
$this->vociSpesa[$i]['percentuale_inquilino'] = $inq;
|
|
$this->vociSpesa[$i]['modifica'] = true;
|
|
}
|
|
|
|
$this->presetDirty = false;
|
|
}
|
|
|
|
public function savePresetAsNew(): void
|
|
{
|
|
if (! $this->stabileId || ! Schema::hasTable('ripartizione_presets')) {
|
|
return;
|
|
}
|
|
|
|
$nome = trim($this->presetNome);
|
|
if ($nome === '') {
|
|
Notification::make()->title('Inserisci un nome per il preset')->warning()->send();
|
|
return;
|
|
}
|
|
|
|
$data = array_values(array_map(function ($row): array {
|
|
return [
|
|
'codice' => $row['codice'] ?? null,
|
|
'percentuale_condomino' => is_numeric($row['percentuale_condomino'] ?? null) ? (float) $row['percentuale_condomino'] : null,
|
|
'percentuale_inquilino' => is_numeric($row['percentuale_inquilino'] ?? null) ? (float) $row['percentuale_inquilino'] : null,
|
|
];
|
|
}, $this->vociSpesa));
|
|
|
|
$preset = RipartizionePreset::query()->create([
|
|
'stabile_id' => $this->stabileId,
|
|
'nome' => $nome,
|
|
'dati' => $data,
|
|
]);
|
|
|
|
$this->presetNome = '';
|
|
$this->loadPresetOptions();
|
|
$this->presetId = (int) $preset->id;
|
|
$this->presetRipartizione = 'custom';
|
|
$this->presetDirty = false;
|
|
|
|
Notification::make()->title('Preset creato')->success()->send();
|
|
}
|
|
|
|
public function updatePreset(): void
|
|
{
|
|
if (! $this->stabileId || ! $this->presetId || ! Schema::hasTable('ripartizione_presets')) {
|
|
return;
|
|
}
|
|
|
|
$preset = RipartizionePreset::query()
|
|
->where('stabile_id', $this->stabileId)
|
|
->whereKey($this->presetId)
|
|
->first();
|
|
|
|
if (! $preset) {
|
|
return;
|
|
}
|
|
|
|
$data = array_values(array_map(function ($row): array {
|
|
return [
|
|
'codice' => $row['codice'] ?? null,
|
|
'percentuale_condomino' => is_numeric($row['percentuale_condomino'] ?? null) ? (float) $row['percentuale_condomino'] : null,
|
|
'percentuale_inquilino' => is_numeric($row['percentuale_inquilino'] ?? null) ? (float) $row['percentuale_inquilino'] : null,
|
|
];
|
|
}, $this->vociSpesa));
|
|
|
|
$preset->update(['dati' => $data]);
|
|
$this->presetDirty = false;
|
|
|
|
Notification::make()->title('Preset aggiornato')->success()->send();
|
|
}
|
|
|
|
public function updatedVociSpesa($value, string $name): void
|
|
{
|
|
if (! str_contains($name, 'percentuale_')) {
|
|
return;
|
|
}
|
|
|
|
if (! preg_match('/^vociSpesa\.(\d+)\.(percentuale_condomino|percentuale_inquilino)$/', $name, $m)) {
|
|
return;
|
|
}
|
|
|
|
$index = (int) $m[1];
|
|
if (! isset($this->vociSpesa[$index])) {
|
|
return;
|
|
}
|
|
|
|
if (empty($this->vociSpesa[$index]['modifica'])) {
|
|
return;
|
|
}
|
|
|
|
$val = is_numeric($value) ? (float) $value : 0.0;
|
|
$val = max(0, min(100, $val));
|
|
|
|
if ($m[2] === 'percentuale_condomino') {
|
|
$this->vociSpesa[$index]['percentuale_condomino'] = $val;
|
|
$this->vociSpesa[$index]['percentuale_inquilino'] = round(100 - $val, 2);
|
|
} else {
|
|
$this->vociSpesa[$index]['percentuale_inquilino'] = $val;
|
|
$this->vociSpesa[$index]['percentuale_condomino'] = round(100 - $val, 2);
|
|
}
|
|
|
|
if ($this->presetRipartizione === 'custom' && $this->presetId) {
|
|
$this->presetDirty = true;
|
|
}
|
|
}
|
|
|
|
private function mapVoceCategoriaToConfedilizia(string $categoriaVoce): string
|
|
{
|
|
return match ($categoriaVoce) {
|
|
'riscaldamento' => 'B',
|
|
'ascensore' => 'C',
|
|
'illuminazione' => 'D',
|
|
'pulizia' => 'E',
|
|
'acqua' => 'G',
|
|
'energia_elettrica' => 'L',
|
|
'gas' => 'B',
|
|
'giardino_verde' => 'I',
|
|
'sicurezza' => 'N',
|
|
'amministrazione' => 'O',
|
|
'assicurazioni' => 'A',
|
|
'tasse_tributi' => 'O',
|
|
'spese_legali' => 'O',
|
|
'manutenzione_ordinaria' => 'A',
|
|
'manutenzione_straordinaria' => 'A',
|
|
'lavori_miglioramento' => 'A',
|
|
default => 'A',
|
|
};
|
|
}
|
|
|
|
public function saveVociSpesa(): void
|
|
{
|
|
if (! $this->stabileId || ! $this->tabellaId) {
|
|
return;
|
|
}
|
|
|
|
if (! Schema::hasTable('voci_spesa')) {
|
|
return;
|
|
}
|
|
|
|
$hasCond = Schema::hasColumn('voci_spesa', 'percentuale_condomino');
|
|
$hasInq = Schema::hasColumn('voci_spesa', 'percentuale_inquilino');
|
|
|
|
$validated = Validator::make(
|
|
['voci' => $this->vociSpesa],
|
|
[
|
|
'voci' => ['array'],
|
|
'voci.*.id' => ['required', 'integer', 'min:1'],
|
|
'voci.*.percentuale_condomino' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
|
'voci.*.percentuale_inquilino' => ['nullable', 'numeric', 'min:0', 'max:100'],
|
|
]
|
|
)->validate();
|
|
|
|
$rows = (array) ($validated['voci'] ?? []);
|
|
if ($rows === []) {
|
|
return;
|
|
}
|
|
|
|
foreach ($rows as $r) {
|
|
$id = (int) ($r['id'] ?? 0);
|
|
if ($id <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if (empty($r['modifica'])) {
|
|
continue;
|
|
}
|
|
|
|
$payload = [];
|
|
if ($hasCond && array_key_exists('percentuale_condomino', $r)) {
|
|
$payload['percentuale_condomino'] = is_numeric($r['percentuale_condomino']) ? (float) $r['percentuale_condomino'] : null;
|
|
}
|
|
if ($hasInq && array_key_exists('percentuale_inquilino', $r)) {
|
|
$payload['percentuale_inquilino'] = is_numeric($r['percentuale_inquilino']) ? (float) $r['percentuale_inquilino'] : null;
|
|
}
|
|
|
|
if ($payload === []) {
|
|
continue;
|
|
}
|
|
|
|
VoceSpesa::query()
|
|
->where('stabile_id', $this->stabileId)
|
|
->where('tabella_millesimale_default_id', $this->tabellaId)
|
|
->whereKey($id)
|
|
->update($payload);
|
|
}
|
|
|
|
$this->loadVociSpesa();
|
|
|
|
Notification::make()
|
|
->title('Voci aggiornate')
|
|
->success()
|
|
->send();
|
|
|
|
if ($this->presetRipartizione === 'custom' && $this->presetId) {
|
|
$this->presetDirty = true;
|
|
Notification::make()
|
|
->title('Preset modificato')
|
|
->body('Aggiorna il preset selezionato o creane uno nuovo.')
|
|
->warning()
|
|
->send();
|
|
}
|
|
}
|
|
|
|
private function formatCodiceUnita(?string $codice): ?string
|
|
{
|
|
if (! is_string($codice)) {
|
|
return null;
|
|
}
|
|
|
|
$codice = trim($codice);
|
|
if ($codice === '') {
|
|
return null;
|
|
}
|
|
|
|
$prefix = trim((string) ($this->codicePrefix ?? ''));
|
|
|
|
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.*.id' => ['nullable', 'integer'],
|
|
'righe.*.unita_id' => ['required', 'integer', 'min:1'],
|
|
'righe.*.millesimi' => ['required', 'numeric', 'min:0'],
|
|
'righe.*.partecipa' => ['nullable', 'boolean'],
|
|
'righe.*.nord' => ['nullable', 'integer'],
|
|
'righe.*.ruolo_legacy' => ['nullable', 'string', 'max:100'],
|
|
]
|
|
)->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;
|
|
$nord = isset($r['nord']) && is_numeric($r['nord']) ? (int) $r['nord'] : null;
|
|
$ruolo = isset($r['ruolo_legacy']) ? trim((string) $r['ruolo_legacy']) : null;
|
|
$id = isset($r['id']) ? (int) $r['id'] : null;
|
|
|
|
$exists = null;
|
|
if ($id) {
|
|
$exists = DettaglioMillesimi::find($id);
|
|
}
|
|
if (! $exists) {
|
|
$exists = DettaglioMillesimi::query()
|
|
->where('tabella_millesimale_id', $this->tabellaId)
|
|
->where('unita_immobiliare_id', $unitaId)
|
|
->where('ruolo_legacy', $ruolo)
|
|
->first();
|
|
}
|
|
|
|
if ($exists) {
|
|
$exists->update([
|
|
'millesimi' => $millesimi,
|
|
'partecipa' => $partecipa,
|
|
'nord' => $nord,
|
|
'ruolo_legacy' => $ruolo ?: null,
|
|
]);
|
|
continue;
|
|
}
|
|
|
|
DettaglioMillesimi::query()->create([
|
|
'tabella_millesimale_id' => (int) $this->tabellaId,
|
|
'unita_immobiliare_id' => $unitaId,
|
|
'millesimi' => $millesimi,
|
|
'partecipa' => $partecipa,
|
|
'nord' => $nord,
|
|
'ruolo_legacy' => $ruolo ?: null,
|
|
'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;
|
|
}
|
|
}
|