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('allinea_contratto_acqua') ->label('Allinea contratto acqua') ->icon('heroicon-o-arrow-path') ->action(function (): void { $this->syncAcquaContract(); }), 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(); } public function getAcquaSnapshotProperty(): array { $stabileId = (int) ($this->stabileAttivo?->id ?? 0); if ($stabileId <= 0) { return [ 'servizio_nome' => null, 'fornitore' => null, 'utenza' => null, 'cliente' => null, 'contratto' => null, 'matricola' => null, 'contratto_hub_id' => null, ]; } $servizio = StabileServizio::query() ->with('fornitore:id,ragione_sociale,nome,cognome') ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->orderByDesc('attivo') ->orderBy('id') ->first(); $contratto = StabileContratto::query() ->where('stabile_id', $stabileId) ->where('tipo_contratto', 'acqua') ->orderByRaw("CASE WHEN stato = 'attivo' THEN 0 ELSE 1 END") ->orderByDesc('id') ->first(); $fornitoreLabel = trim((string) ($servizio?->fornitore?->ragione_sociale ?: trim(($servizio?->fornitore?->nome ?? '') . ' ' . ($servizio?->fornitore?->cognome ?? '')))); return [ 'servizio_nome' => $servizio?->nome, 'fornitore' => $fornitoreLabel !== '' ? $fornitoreLabel : null, 'utenza' => $servizio?->codice_utenza, 'cliente' => $servizio?->codice_cliente, 'contratto' => $servizio?->codice_contratto, 'matricola' => $servizio?->contatore_matricola, 'contratto_hub_id' => $contratto?->id, ]; } /** @return array */ public function getAcquaExtractionDiagnosticsProperty(): array { $stabileId = (int) ($this->stabileAttivo?->id ?? 0); if ($stabileId <= 0) { return ['latest' => null, 'counts' => ['totale' => 0, 'con_identificativi' => 0, 'senza_identificativi' => 0, 'con_pagamento' => 0], 'warnings' => []]; } $servizio = StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->orderByDesc('attivo') ->orderBy('id') ->first(['id', 'codice_utenza', 'codice_cliente', 'codice_contratto', 'contatore_matricola']); $rows = FatturaElettronica::query() ->where('stabile_id', $stabileId) ->whereNotNull('consumo_raw') ->orderByDesc('data_fattura') ->orderByDesc('id') ->limit(30) ->get(['id', 'numero_fattura', 'data_fattura', 'consumo_raw']) ->map(function (FatturaElettronica $fattura): ?array { $payload = json_decode((string) $fattura->consumo_raw, true); if (! $this->payloadLooksLikeAcqua(is_array($payload) ? $payload : null)) { return null; } return [ 'id' => (int) $fattura->id, 'numero_fattura' => trim((string) ($fattura->numero_fattura ?? '')), 'data_fattura' => optional($fattura->data_fattura)->format('d/m/Y'), 'utenza' => trim((string) data_get($payload, 'codici.utenza', '')) ?: null, 'cliente' => trim((string) data_get($payload, 'codici.cliente', '')) ?: null, 'contratto' => trim((string) data_get($payload, 'codici.contratto', '')) ?: null, 'matricola' => trim((string) data_get($payload, 'contatore.matricola', '')) ?: null, 'cbill' => trim((string) data_get($payload, 'pagamento.cbill', '')) ?: null, 'codice_avviso' => trim((string) data_get($payload, 'pagamento.codice_avviso', '')) ?: null, 'sia' => trim((string) data_get($payload, 'pagamento.sia', '')) ?: null, ]; }) ->filter() ->values(); $latest = $rows->first(); $warnings = []; if ($latest && $servizio instanceof StabileServizio) { foreach ([ 'utenza' => (string) ($servizio->codice_utenza ?? ''), 'cliente' => (string) ($servizio->codice_cliente ?? ''), 'contratto' => (string) ($servizio->codice_contratto ?? ''), 'matricola' => (string) ($servizio->contatore_matricola ?? ''), ] as $key => $currentValue) { $currentValue = trim($currentValue); $extractedValue = trim((string) ($latest[$key] ?? '')); if ($currentValue === '' && $extractedValue !== '') { $warnings[] = 'Manca ' . $key . ' sul servizio acqua ma è presente nell’ultima FE: ' . $extractedValue; } elseif ($currentValue !== '' && $extractedValue !== '' && $this->normalizeIdentifier($currentValue) !== $this->normalizeIdentifier($extractedValue)) { $warnings[] = 'Disallineamento ' . $key . ': servizio=' . $currentValue . ' / FE=' . $extractedValue; } } } return [ 'latest' => $latest, 'counts' => [ 'totale' => $rows->count(), 'con_identificativi' => $rows->filter(fn(array $row): bool => ! empty($row['utenza']) || ! empty($row['cliente']) || ! empty($row['contratto']) || ! empty($row['matricola']))->count(), 'senza_identificativi' => $rows->filter(fn(array $row): bool => empty($row['utenza']) && empty($row['cliente']) && empty($row['contratto']) && empty($row['matricola']))->count(), 'con_pagamento' => $rows->filter(fn(array $row): bool => ! empty($row['cbill']) || ! empty($row['codice_avviso']) || ! empty($row['sia']))->count(), ], 'warnings' => $warnings, ]; } private function payloadLooksLikeAcqua(?array $payload): bool { if (! is_array($payload) || $payload === []) { return false; } if (($payload['type'] ?? null) === 'acqua') { return true; } return is_array($payload['consumi'] ?? null) || is_array($payload['riepilogo_letture'] ?? null) || is_array($payload['finestra_autolettura'] ?? null) || is_array($payload['codici'] ?? null); } private function normalizeIdentifier(string $value): string { return preg_replace('/[^A-Z0-9]/', '', strtoupper(trim($value))) ?? ''; } 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, ]); if ((string) $contract->tipo_contratto === 'acqua') { $servizio = StabileServizio::query() ->with('fornitore:id,ragione_sociale,nome,cognome') ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->where(function ($query) use ($contract): void { $query->where('codice_contratto', $contract->codice_contratto) ->orWhere('codice_utenza', $contract->riferimento_esterno) ->orWhere('codice_cliente', $contract->riferimento_esterno); }) ->orderByDesc('attivo') ->orderBy('id') ->first(); if ($servizio instanceof StabileServizio) { $contract = app(AcquaContractSyncService::class)->syncForService($servizio, (int) $user->id) ?? $contract; } else { $this->syncScadenzeContratto($contract); } } else { $this->syncScadenzeContratto($contract); } Notification::make() ->title('Contratto registrato') ->body('Scadenze programmate: ' . $contract->scadenze()->count()) ->success() ->send(); } protected function syncAcquaContract(): void { $user = Auth::user(); $stabileId = (int) ($this->stabileAttivo?->id ?? 0); if (! $user instanceof User || $stabileId <= 0) { Notification::make()->title('Stabile attivo non disponibile')->danger()->send(); return; } $stats = app(AcquaContractSyncService::class)->normalizeStabile($stabileId, (int) $user->id); if (($stats['services'] ?? 0) <= 0) { Notification::make()->title('Nessun servizio acqua configurato per lo stabile attivo')->warning()->send(); return; } Notification::make() ->title('Contratto acqua allineato') ->body('Servizi sincronizzati: ' . (int) ($stats['synced'] ?? 0) . '. Contratti acqua dopo bonifica: ' . (int) ($stats['contracts_after'] ?? 0) . '.') ->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'); } }