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

667 lines
27 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Filament\Pages\Condomini;
use App\Filament\Pages\Strumenti\DocumentiArchivio;
use App\Models\Documento;
use App\Models\FatturaElettronica;
use App\Models\Fornitore;
use App\Models\Scadenza;
use App\Models\Stabile;
use App\Models\StabileContratto;
use App\Models\StabileServizio;
use App\Models\User;
use App\Services\Consumi\AcquaContractSyncService;
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\Facades\Schema;
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('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<string,mixed> */
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 nellultima 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<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');
}
}