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

471 lines
18 KiB
PHP

<?php
namespace App\Filament\Pages\Condomini;
use App\Filament\Pages\Strumenti\DocumentiArchivio;
use App\Models\Documento;
use App\Models\Fornitore;
use App\Models\Scadenza;
use App\Models\Stabile;
use App\Models\StabileContratto;
use App\Models\User;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Repeater;
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 Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use UnitEnum;
class ContrattiHub extends Page
{
protected static ?string $navigationLabel = 'HUB Contratti';
protected static ?string $title = 'HUB Contratti';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
protected static ?int $navigationSort = 34;
protected static ?string $slug = 'condomini/contratti-hub';
protected string $view = 'filament.pages.condomini.contratti-hub';
public ?Stabile $stabileAttivo = null;
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) {
return;
}
$activeId = StabileContext::resolveActiveStabileId($user);
if (! $activeId) {
return;
}
$this->stabileAttivo = StabileContext::accessibleStabili($user)->firstWhere('id', $activeId);
}
protected function getHeaderActions(): array
{
return [
Action::make('registra_contratto')
->label('Registra contratto')
->icon('heroicon-o-plus')
->form($this->getContrattoFormSchema())
->action(function (array $data): void {
$this->createContratto($data);
}),
Action::make('apri_documenti')
->label('Archivio documenti')
->icon('heroicon-o-folder-open')
->url(fn(): string => DocumentiArchivio::getUrl(panel: 'admin-filament')),
];
}
public function getStatsProperty(): array
{
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
if ($stabileId <= 0) {
return ['totale' => 0, 'attivi' => 0, 'scadenze' => 0, 'documenti' => 0];
}
$query = StabileContratto::query()->where('stabile_id', $stabileId);
return [
'totale' => (clone $query)->count(),
'attivi' => (clone $query)->where('stato', 'attivo')->count(),
'scadenze' => Scadenza::query()
->where('stabile_id', $stabileId)
->whereNotNull('stabile_contratto_id')
->whereDate('data_scadenza', '>=', now()->toDateString())
->whereDate('data_scadenza', '<=', now()->addDays(45)->toDateString())
->count(),
'documenti' => Documento::query()
->where('stabile_id', $stabileId)
->where('tipo_documento', 'contratto')
->count(),
];
}
public function getContrattiRowsProperty(): Collection
{
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
if ($stabileId <= 0) {
return collect();
}
return StabileContratto::query()
->with(['fornitore:id,ragione_sociale,nome,cognome', 'documentoPrincipale:id,nome,numero_protocollo'])
->where('stabile_id', $stabileId)
->orderByRaw("CASE WHEN stato = 'attivo' THEN 0 WHEN stato = 'in_revisione' THEN 1 ELSE 2 END")
->orderByDesc('decorrenza_dal')
->orderByDesc('id')
->get();
}
public function getUpcomingScadenzeProperty(): Collection
{
$stabileId = (int) ($this->stabileAttivo?->id ?? 0);
if ($stabileId <= 0) {
return collect();
}
return Scadenza::query()
->with('stabileContratto:id,titolo,tipo_contratto')
->where('stabile_id', $stabileId)
->whereNotNull('stabile_contratto_id')
->whereDate('data_scadenza', '>=', now()->subDays(7)->toDateString())
->orderBy('data_scadenza')
->limit(10)
->get();
}
protected function getContrattoFormSchema(): array
{
return [
Select::make('stabile_id')
->label('Stabile')
->options(fn(): array=> $this->getStabiliOptions())
->default(fn(): ?int => $this->stabileAttivo?->id)
->searchable()
->required(),
Select::make('fornitore_id')
->label('Fornitore / manutentore')
->options(fn(): array=> $this->getFornitoriOptions())
->searchable()
->nullable(),
Select::make('tipo_contratto')
->label('Tipo contratto')
->options($this->getTipoContrattoOptions())
->required(),
TextInput::make('titolo')
->label('Titolo')
->required()
->maxLength(255),
TextInput::make('servizio_label')
->label('Servizio / utenza')
->placeholder('Es. Ascensore scala A, Acqua potabile, Luce comune')
->maxLength(255),
TextInput::make('codice_contratto')
->label('Codice contratto')
->maxLength(80),
TextInput::make('riferimento_esterno')
->label('Riferimento esterno')
->placeholder('Es. numero cliente, riferimento pratica, ID POD/PDR principale')
->maxLength(120),
DatePicker::make('data_stipula')
->label('Data stipula'),
DatePicker::make('decorrenza_dal')
->label('Decorrenza dal')
->required(),
DatePicker::make('decorrenza_al')
->label('Decorrenza al'),
Select::make('frequenza_scadenze')
->label('Frequenza scadenze')
->options($this->getFrequenzaOptions())
->default('annuale')
->required(),
TextInput::make('giorno_scadenza')
->label('Giorno scadenza')
->numeric()
->minValue(1)
->maxValue(28),
TextInput::make('giorni_preavviso')
->label('Giorni preavviso')
->numeric()
->default(30)
->minValue(0)
->maxValue(365),
Toggle::make('rinnovo_automatico')
->label('Rinnovo automatico')
->default(false),
TextInput::make('importo_periodico')
->label('Importo periodico')
->numeric()
->step(0.01)
->minValue(0),
Select::make('documento_principale_id')
->label('Documento principale')
->options(fn(callable $get): array=> $this->getDocumentiContrattoOptions((int) ($get('stabile_id') ?: 0)))
->searchable()
->nullable(),
Repeater::make('codici_collegati')
->label('Codici collegati')
->schema([
TextInput::make('chiave')
->label('Chiave')
->placeholder('Es. POD, PDR, matricola_contatore, codice_cliente, utenza, matricola_ascensore')
->required(),
TextInput::make('valore')
->label('Valore')
->required(),
])
->addActionLabel('Aggiungi codice')
->defaultItems(0)
->columns(2),
Textarea::make('note')
->label('Note')
->rows(4),
];
}
protected function createContratto(array $data): void
{
$user = Auth::user();
if (! $user instanceof User) {
Notification::make()->title('Utente non valido')->danger()->send();
return;
}
$stabileId = (int) ($data['stabile_id'] ?? 0);
$stabile = StabileContext::accessibleStabili($user)->firstWhere('id', $stabileId);
if (! $stabile instanceof Stabile) {
Notification::make()->title('Stabile non valido')->danger()->send();
return;
}
$contract = StabileContratto::query()->create([
'stabile_id' => $stabileId,
'fornitore_id' => isset($data['fornitore_id']) && is_numeric($data['fornitore_id']) ? (int) $data['fornitore_id'] : null,
'documento_principale_id' => isset($data['documento_principale_id']) && is_numeric($data['documento_principale_id']) ? (int) $data['documento_principale_id'] : null,
'titolo' => trim((string) ($data['titolo'] ?? '')),
'tipo_contratto' => trim((string) ($data['tipo_contratto'] ?? 'altro')),
'categoria_impianto' => trim((string) ($data['tipo_contratto'] ?? 'altro')),
'servizio_label' => $this->nullableString($data['servizio_label'] ?? null),
'codice_contratto' => $this->nullableString($data['codice_contratto'] ?? null),
'riferimento_esterno' => $this->nullableString($data['riferimento_esterno'] ?? null),
'stato' => 'attivo',
'data_stipula' => $data['data_stipula'] ?? null,
'decorrenza_dal' => $data['decorrenza_dal'] ?? null,
'decorrenza_al' => $data['decorrenza_al'] ?? null,
'frequenza_scadenze' => $this->nullableString($data['frequenza_scadenze'] ?? null),
'giorno_scadenza' => isset($data['giorno_scadenza']) && is_numeric($data['giorno_scadenza']) ? (int) $data['giorno_scadenza'] : null,
'giorni_preavviso' => isset($data['giorni_preavviso']) && is_numeric($data['giorni_preavviso']) ? (int) $data['giorni_preavviso'] : 30,
'rinnovo_automatico' => (bool) ($data['rinnovo_automatico'] ?? false),
'importo_periodico' => isset($data['importo_periodico']) && is_numeric($data['importo_periodico']) ? (float) $data['importo_periodico'] : null,
'valuta' => 'EUR',
'codici_collegati' => $this->normalizeKeyValueRows($data['codici_collegati'] ?? []),
'dati_contratto' => [
'origine' => 'hub_contratti_stabile',
'documenti_url' => DocumentiArchivio::getUrl(panel: 'admin-filament'),
],
'note' => $this->nullableString($data['note'] ?? null),
'created_by' => (int) $user->id,
'updated_by' => (int) $user->id,
]);
$this->syncScadenzeContratto($contract);
Notification::make()
->title('Contratto registrato')
->body('Scadenze programmate: ' . $contract->scadenze()->count())
->success()
->send();
}
protected function syncScadenzeContratto(StabileContratto $contract): void
{
$contract->scadenze()->delete();
foreach ($this->buildScheduleDates($contract) as $date) {
$popupDal = $date->copy()->subDays(max(0, (int) ($contract->giorni_preavviso ?? 0)));
Scadenza::query()->create([
'stabile_id' => (int) $contract->stabile_id,
'stabile_contratto_id' => (int) $contract->id,
'descrizione_sintetica' => $contract->titolo,
'data_scadenza' => $date->toDateString(),
'natura' => 'contratto_stabile',
'natura_label' => 'Scadenza contratto',
'gestione_tipo' => $contract->tipo_contratto,
'fatto' => 'N',
'tipo_riga' => trim((string) ($contract->servizio_label ?: $contract->tipo_contratto)),
'richiede_pop_up' => true,
'giorni_anticipo' => (int) ($contract->giorni_preavviso ?? 0),
'popup_dal' => $popupDal->toDateString(),
'legacy_payload' => [
'source' => 'stabile_contratti',
'codice_contratto' => $contract->codice_contratto,
'riferimento_esterno' => $contract->riferimento_esterno,
'codici_collegati' => $contract->codici_collegati ?? [],
],
]);
}
}
/** @return array<int, Carbon> */
protected function buildScheduleDates(StabileContratto $contract): array
{
$start = $contract->decorrenza_dal instanceof Carbon
? $contract->decorrenza_dal->copy()
: ($contract->decorrenza_al instanceof Carbon ? $contract->decorrenza_al->copy() : now()->copy());
$end = $contract->decorrenza_al instanceof Carbon ? $contract->decorrenza_al->copy() : null;
$frequency = trim((string) ($contract->frequenza_scadenze ?? 'una_tantum'));
if ($contract->giorno_scadenza) {
$start->day(min(28, (int) $contract->giorno_scadenza));
}
if ($frequency === '' || $frequency === 'una_tantum') {
return [$end ?: $start];
}
$dates = [];
$cursor = $start->copy();
$limit = $end
? $end->copy()
: ($contract->rinnovo_automatico ? $start->copy()->addMonths(24) : $start->copy()->addMonths(12));
while ($cursor <= $limit && count($dates) < 60) {
$dates[] = $cursor->copy();
$cursor = match ($frequency) {
'mensile' => $cursor->copy()->addMonthNoOverflow(),
'bimestrale' => $cursor->copy()->addMonthsNoOverflow(2),
'trimestrale' => $cursor->copy()->addMonthsNoOverflow(3),
'semestrale' => $cursor->copy()->addMonthsNoOverflow(6),
default => $cursor->copy()->addYearNoOverflow(),
};
if ($contract->giorno_scadenza) {
$cursor->day(min(28, (int) $contract->giorno_scadenza));
}
}
return $dates;
}
protected function getTipoContrattoOptions(): array
{
return [
'acqua' => 'Acqua',
'energia_elettrica' => 'Energia elettrica',
'ascensore' => 'Ascensore',
'gas' => 'Gas',
'pulizie' => 'Pulizie',
'portierato' => 'Portierato',
'assicurazione' => 'Assicurazione',
'antincendio' => 'Antincendio',
'raffrescamento' => 'Raffrescamento',
'riscaldamento' => 'Riscaldamento',
'telefonia_dati' => 'Telefonia / dati',
'altro' => 'Altro servizio',
];
}
protected function getFrequenzaOptions(): array
{
return [
'una_tantum' => 'Una tantum',
'mensile' => 'Mensile',
'bimestrale' => 'Bimestrale',
'trimestrale' => 'Trimestrale',
'semestrale' => 'Semestrale',
'annuale' => 'Annuale',
];
}
protected function getStabiliOptions(): array
{
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
return StabileContext::accessibleStabili($user)
->mapWithKeys(fn(Stabile $stabile): array=> [(string) $stabile->id => trim((string) ($stabile->denominazione ?: ('Stabile #' . $stabile->id)))])
->all();
}
protected function getFornitoriOptions(): array
{
return Fornitore::query()
->orderByRaw("COALESCE(NULLIF(ragione_sociale, ''), CONCAT(COALESCE(nome, ''), ' ', COALESCE(cognome, ''))) asc")
->limit(1500)
->get(['id', 'ragione_sociale', 'nome', 'cognome'])
->mapWithKeys(function (Fornitore $fornitore): array {
$label = trim((string) ($fornitore->ragione_sociale ?: trim(($fornitore->nome ?? '') . ' ' . ($fornitore->cognome ?? ''))));
return [(string) $fornitore->id => ($label !== '' ? $label : ('Fornitore #' . $fornitore->id))];
})
->all();
}
protected function getDocumentiContrattoOptions(int $stabileId): array
{
if ($stabileId <= 0) {
return [];
}
return Documento::query()
->where('stabile_id', $stabileId)
->where('tipo_documento', 'contratto')
->orderByDesc('data_documento')
->limit(250)
->get(['id', 'nome', 'numero_protocollo'])
->mapWithKeys(fn(Documento $documento): array=> [
(string) $documento->id => trim((string) (($documento->numero_protocollo ? $documento->numero_protocollo . ' · ' : '') . ($documento->nome ?: ('Documento #' . $documento->id)))),
])
->all();
}
/** @param array<int, mixed> $rows */
protected function normalizeKeyValueRows(array $rows): array
{
return collect($rows)
->map(function ($row): ?array {
$key = trim((string) data_get($row, 'chiave', ''));
$value = trim((string) data_get($row, 'valore', ''));
return ($key !== '' && $value !== '') ? ['chiave' => $key, 'valore' => $value] : null;
})
->filter()
->values()
->all();
}
protected function nullableString(mixed $value): ?string
{
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
public function contractBadgeColor(string $status): string
{
return match ($status) {
'attivo' => 'emerald',
'in_revisione' => 'amber',
'scaduto' => 'rose',
default => 'slate',
};
}
public function contractTypeLabel(?string $type): string
{
$key = trim((string) $type);
return $this->getTipoContrattoOptions()[$key] ?? ($key !== '' ? Str::headline($key) : 'Contratto');
}
}