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 */ 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 $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'); } }