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

577 lines
19 KiB
PHP

<?php
namespace App\Filament\Pages\Condomini;
use App\Filament\Pages\UnitaImmobiliarePage;
use App\Models\Palazzina;
use App\Models\RubricaRuolo;
use App\Models\UnitaImmobiliare;
use App\Models\UnitaTipologia;
use App\Models\User;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use UnitEnum;
class PalazzineInterni extends Page
{
protected int $stabileId = 0;
public ?int $palazzinaId = null;
public ?int $unitaId = null;
/**
* @var array<int, array<string, mixed>>
*/
public array $palazzine = [];
/**
* @var array<string, array<int, array<string, mixed>>> [scala => [piano => units[]]]
*/
public array $unitaPerScalaPiano = [];
/**
* @var array<int, array<string, mixed>>
*/
public array $millesimiPerTabella = [];
/**
* @var array<int>
*/
public array $selectedUnitaIds = [];
public ?string $bulkScala = null;
public ?int $bulkPiano = null;
public ?int $bulkTipologiaId = null;
public ?string $bulkCategoriaCatastale = null;
protected static ?string $navigationLabel = 'Palazzine · Interni';
protected static ?string $title = 'Palazzine · Interni';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-building-office-2';
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
protected static ?int $navigationSort = 31;
protected static ?string $slug = 'condomini/palazzine/interni';
protected string $view = 'filament.pages.condomini.palazzine-interni';
public function getBackUrl(): ?string
{
$candidate = request()->query('back');
if (! is_string($candidate) || trim($candidate) === '') {
return null;
}
$candidate = trim($candidate);
// Allow relative URLs only.
if (str_starts_with($candidate, '/')) {
return $candidate;
}
$parts = parse_url($candidate);
if (! is_array($parts)) {
return null;
}
$host = $parts['host'] ?? null;
if ($host && $host !== request()->getHost()) {
return null;
}
$path = $parts['path'] ?? null;
if (! is_string($path) || $path === '') {
return null;
}
$query = isset($parts['query']) ? ('?' . $parts['query']) : '';
return $path . $query;
}
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}
public function mount(): void
{
$user = Auth::user();
if (! $user instanceof User) {
abort(403);
}
$requestedStabileId = request()->integer('stabile_id');
if ($requestedStabileId > 0) {
StabileContext::setActiveStabileId($user, (int) $requestedStabileId);
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
Notification::make()
->title('Nessuno stabile disponibile')
->body('Seleziona uno stabile per vedere palazzine e unità.')
->warning()
->send();
return;
}
$this->stabileId = $stabileId;
$this->loadPalazzine();
$requestedPalazzinaId = request()->integer('palazzina_id') ?: null;
$this->palazzinaId = $this->pickPalazzinaId($requestedPalazzinaId);
$requestedUnitaId = request()->integer('unita_id') ?: null;
$this->unitaId = $requestedUnitaId;
$this->loadUnitaGrid();
$this->loadMillesimiUnita();
}
public function hydrate(): void
{
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if ($stabileId) {
$this->stabileId = (int) $stabileId;
}
}
protected function loadPalazzine(): void
{
$this->palazzine = Palazzina::query()
->where('stabile_id', $this->stabileId)
->orderBy('codice_palazzina')
->orderBy('denominazione')
->get(['id', 'codice_palazzina', 'denominazione', 'numero_scale', 'numero_piani_fuori_terra', 'numero_piani_interrati', 'ha_piano_terra'])
->map(function (Palazzina $p) {
$codice = $p->codice_palazzina ?: ('PAL ' . $p->id);
$nome = $p->denominazione ?: 'Palazzina';
return [
'id' => (int) $p->id,
'codice' => $codice,
'nome' => $nome,
'scale' => (int) ($p->numero_scale ?? 0),
'piani_fuori_terra' => (int) ($p->numero_piani_fuori_terra ?? 0),
'piani_interrati' => (int) ($p->numero_piani_interrati ?? 0),
'ha_piano_terra' => (bool) ($p->ha_piano_terra ?? false),
];
})
->values()
->all();
}
protected function pickPalazzinaId(?int $candidate): ?int
{
if ($candidate && collect($this->palazzine)->contains(fn($p) => (int) $p['id'] === $candidate)) {
return $candidate;
}
$first = $this->palazzine[0]['id'] ?? null;
return is_numeric($first) ? (int) $first : null;
}
protected function loadUnitaGrid(): void
{
$this->unitaPerScalaPiano = [];
if (! $this->palazzinaId) {
return;
}
$unita = UnitaImmobiliare::query()
->where('stabile_id', $this->stabileId)
->where('palazzina_id', $this->palazzinaId)
->orderBy('scala')
->orderBy('piano')
->orderBy('id')
->get(['id', 'codice_unita', 'denominazione', 'scala', 'piano', 'interno']);
$ownersByUnita = [];
$tenantsByUnita = [];
// Preferisci Rubrica (nuovo archivio) se disponibile.
try {
$ids = $unita->pluck('id')->map(fn($v) => (int) $v)->values()->all();
if (! empty($ids) && DB::getSchemaBuilder()->hasTable('rubrica_ruoli')) {
$today = now()->toDateString();
$ruoli = RubricaRuolo::query()
->with(['contatto'])
->where('stabile_id', $this->stabileId)
->whereIn('unita_immobiliare_id', $ids)
->where('is_attivo', true)
->where(function (Builder $q) use ($today) {
$q->whereNull('data_inizio')->orWhere('data_inizio', '<=', $today);
})
->where(function (Builder $q) use ($today) {
$q->whereNull('data_fine')->orWhere('data_fine', '>=', $today);
})
->orderByDesc('is_preferito')
->orderByDesc('data_inizio')
->orderByDesc('id')
->limit(5000)
->get();
foreach ($ruoli as $ruolo) {
$unitaId = (int) ($ruolo->unita_immobiliare_id ?? 0);
if (! $unitaId) {
continue;
}
$label = (string) ($ruolo->contatto?->nome_completo ?? '');
$label = trim($label);
if ($label === '') {
continue;
}
$roleKey = strtolower(trim((string) ($ruolo->ruolo_standard ?? '')));
if ($roleKey === 'inquilino') {
$tenantsByUnita[$unitaId] ??= $label;
} else {
$ownersByUnita[$unitaId] ??= $label;
}
}
}
} catch (\Throwable $e) {
}
// Fallback legacy (se presente): proprieta+soggetti
try {
$ids = $unita->pluck('id')->map(fn($v) => (int) $v)->values()->all();
if (! empty($ids) && DB::getSchemaBuilder()->hasTable('proprieta')) {
$rows = DB::table('proprieta')
->join('soggetti', 'soggetti.id', '=', 'proprieta.soggetto_id')
->whereIn('proprieta.unita_immobiliare_id', $ids)
->select([
'proprieta.unita_immobiliare_id as unita_id',
'proprieta.tipo_diritto as tipo_diritto',
'soggetti.ragione_sociale as ragione_sociale',
'soggetti.cognome as cognome',
'soggetti.nome as nome',
])
->get();
foreach ($rows as $row) {
$unitaId = (int) ($row->unita_id ?? 0);
if (! $unitaId) {
continue;
}
$label = trim((string) ($row->ragione_sociale ?? ''));
if ($label === '') {
$label = trim(((string) ($row->cognome ?? '')) . ' ' . ((string) ($row->nome ?? '')));
}
if ($label === '') {
continue;
}
$tipo = (string) ($row->tipo_diritto ?? '');
if ($tipo === 'locazione') {
$tenantsByUnita[$unitaId] ??= $label;
} else {
$ownersByUnita[$unitaId] ??= $label;
}
}
}
} catch (\Throwable $e) {
}
$grouped = [];
foreach ($unita as $u) {
$scala = (string) ($u->scala ?? '-');
$piano = is_null($u->piano) ? 0 : (int) $u->piano;
$grouped[$scala] ??= [];
$grouped[$scala][$piano] ??= [];
$grouped[$scala][$piano][] = [
'id' => (int) $u->id,
'codice' => $u->codice_unita ?: ('ID ' . $u->id),
'nome' => $u->denominazione ?: 'Unità',
'scala' => $scala,
'piano' => $piano,
'interno' => $u->interno !== null ? (string) $u->interno : '—',
'proprietario' => $ownersByUnita[(int) $u->id] ?? null,
'inquilino' => $tenantsByUnita[(int) $u->id] ?? null,
];
}
// Ordina gli interni in modo naturale (1,2,10) per ogni scala/piano
foreach ($grouped as $scala => $piani) {
foreach ($piani as $piano => $units) {
usort($units, function (array $a, array $b): int {
$ka = $this->internoSortKey($a['interno'] ?? null);
$kb = $this->internoSortKey($b['interno'] ?? null);
if ($ka[0] !== $kb[0]) {
return strcmp($ka[0], $kb[0]);
}
if ($ka[1] !== $kb[1]) {
return $ka[1] <=> $kb[1];
}
if ($ka[2] !== $kb[2]) {
return strnatcasecmp($ka[2], $kb[2]);
}
return ((int)($a['id'] ?? 0)) <=> ((int)($b['id'] ?? 0));
});
$grouped[$scala][$piano] = $units;
}
}
// Ordina piani dall'alto al basso per ogni scala
foreach ($grouped as $scala => $piani) {
krsort($piani);
$grouped[$scala] = $piani;
}
ksort($grouped);
$this->unitaPerScalaPiano = $grouped;
// Se unità selezionata non appartiene alla palazzina corrente, reset
if ($this->unitaId) {
$exists = $unita->contains(fn(UnitaImmobiliare $u) => (int) $u->id === (int) $this->unitaId);
if (! $exists) {
$this->unitaId = null;
}
}
}
/**
* @return array<string, string>
*/
public function getTipologieOptions(): array
{
try {
if (! DB::getSchemaBuilder()->hasTable('unita_tipologie')) {
return [];
}
return UnitaTipologia::query()
->orderBy('categoria')
->orderBy('nome')
->get(['id', 'codice', 'nome'])
->mapWithKeys(function (UnitaTipologia $t): array {
$cod = trim((string) ($t->codice ?? ''));
$name = trim((string) ($t->nome ?? ''));
$label = trim(($cod !== '' ? ($cod . ' - ') : '') . ($name !== '' ? $name : ('Tipologia #' . (int) $t->id)));
return [(string) $t->id => $label];
})
->all();
} catch (\Throwable) {
return [];
}
}
public function bulkSetTipologiaCatasto(): void
{
$ids = is_array($this->selectedUnitaIds ?? null)
? array_values(array_filter(array_map('intval', $this->selectedUnitaIds), fn(int $v) => $v > 0))
: [];
if ($ids === []) {
Notification::make()->title('Nessuna unità selezionata')->warning()->send();
return;
}
$tipologiaId = $this->bulkTipologiaId ? (int) $this->bulkTipologiaId : null;
$cat = is_string($this->bulkCategoriaCatastale) ? trim($this->bulkCategoriaCatastale) : '';
$cat = $cat !== '' ? $cat : null;
if (! $tipologiaId && ! $cat) {
Notification::make()->title('Nessun valore da applicare')->warning()->send();
return;
}
$payload = [];
if ($tipologiaId) {
$payload['tipologia_id'] = $tipologiaId;
}
if ($cat) {
$payload['categoria_catastale'] = $cat;
}
UnitaImmobiliare::query()
->where('stabile_id', $this->stabileId)
->whereIn('id', $ids)
->update($payload);
Notification::make()->title('Unità aggiornate')->success()->send();
$this->loadUnitaGrid();
$this->loadMillesimiUnita();
}
/**
* @return array{0:string,1:int,2:string}
*/
protected function internoSortKey(?string $interno): array
{
$s = trim((string)($interno ?? ''));
if ($s === '' || $s === '—') {
return ['~', PHP_INT_MAX, ''];
}
$u = strtoupper($s);
$m = null;
if (preg_match('/^([A-Z]+)\s*[-\/]\s*(\d+)(.*)$/', $u, $m)) {
return [$m[1], (int)$m[2], trim((string)($m[3] ?? ''))];
}
if (preg_match('/^(\d+)(.*)$/', $u, $m)) {
return ['', (int)$m[1], trim((string)($m[2] ?? ''))];
}
return [$u, PHP_INT_MAX, $u];
}
public function bulkSetPiano(): void
{
if (! $this->stabileId || ! $this->palazzinaId) {
Notification::make()->title('Seleziona una palazzina')->warning()->send();
return;
}
$ids = array_values(array_unique(array_filter(array_map('intval', $this->selectedUnitaIds))));
if (empty($ids)) {
Notification::make()->title('Nessuna unità selezionata')->warning()->send();
return;
}
if ($this->bulkPiano === null) {
Notification::make()->title('Imposta un piano')->warning()->send();
return;
}
$updated = UnitaImmobiliare::query()
->where('stabile_id', $this->stabileId)
->where('palazzina_id', $this->palazzinaId)
->whereIn('id', $ids)
->update([
'piano' => $this->bulkPiano,
'updated_at' => now(),
]);
$this->selectedUnitaIds = [];
$this->loadUnitaGrid();
Notification::make()
->title('Piano associato')
->body("Aggiornate {$updated} unità.")
->success()
->send();
}
public function bulkSetScala(): void
{
if (! $this->stabileId || ! $this->palazzinaId) {
Notification::make()->title('Seleziona una palazzina')->warning()->send();
return;
}
$ids = array_values(array_unique(array_filter(array_map('intval', $this->selectedUnitaIds))));
if (empty($ids)) {
Notification::make()->title('Nessuna unità selezionata')->warning()->send();
return;
}
$scala = is_string($this->bulkScala) ? trim($this->bulkScala) : '';
if ($scala === '') {
Notification::make()->title('Imposta una scala')->warning()->send();
return;
}
$updated = UnitaImmobiliare::query()
->where('stabile_id', $this->stabileId)
->where('palazzina_id', $this->palazzinaId)
->whereIn('id', $ids)
->update([
'scala' => $scala,
'updated_at' => now(),
]);
$this->selectedUnitaIds = [];
$this->loadUnitaGrid();
Notification::make()
->title('Scala associata')
->body("Aggiornate {$updated} unità.")
->success()
->send();
}
protected function loadMillesimiUnita(): void
{
$this->millesimiPerTabella = [];
if (! $this->unitaId) {
return;
}
$unita = UnitaImmobiliare::with(['dettagliMillesimi.tabellaMillesimale'])
->where('stabile_id', $this->stabileId)
->whereKey($this->unitaId)
->first();
if (! $unita) {
return;
}
$rows = $unita->dettagliMillesimi
->filter(fn($d) => $d->tabellaMillesimale !== null)
->sortBy(function ($d) {
$tabella = $d->tabellaMillesimale;
$tipo = (string) ($tabella?->tipo_tabella ?? ($tabella?->tipo ?? ''));
return sprintf('%s|%09d', $tipo, (int) ($tabella?->ordinamento ?? 999999));
})
->map(function ($d) {
$tabella = $d->tabellaMillesimale;
$tipo = (string) ($tabella?->tipo_tabella ?? ($tabella?->tipo ?? ''));
if ($tipo === '') {
$tipo = 'altro';
}
return [
'tipo' => $tipo,
'tabella_id' => (int) ($tabella?->id ?? 0),
'codice' => $tabella?->codice_tabella ?? $tabella?->nome_tabella ?? 'TAB',
'nome' => $tabella?->denominazione ?? $tabella?->nome_tabella_millesimale ?? 'Tabella',
'millesimi' => (float) ($d->millesimi ?? 0),
];
})
->values()
->all();
$this->millesimiPerTabella = $rows;
}
public function getUnitaPageUrl(): ?string
{
if (! $this->unitaId) {
return null;
}
$base = UnitaImmobiliarePage::getUrl(panel: 'admin-filament');
$back = urlencode((string) request()->getRequestUri());
return $base . '?unita_id=' . (int) $this->unitaId . '&back=' . $back;
}
}