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

1766 lines
77 KiB
PHP

<?php
namespace App\Filament\Pages\Condomini;
use App\Filament\Pages\Contabilita\FattureElettronicheP7mRicevute;
use App\Models\Fornitore;
use App\Models\StabileServizio;
use App\Models\StabileServizioLettura;
use App\Models\StabileServizioTariffa;
use App\Models\UnitaImmobiliare;
use App\Models\User;
use App\Models\VoceSpesa;
use App\Support\AnnoGestioneContext;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Actions\Action;
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 Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
use UnitEnum;
class ServiziStabileArchivio extends Page implements HasTable
{
use InteractsWithTable;
protected static ?string $navigationLabel = 'Servizi / Beni comuni';
protected static ?string $title = 'Servizi / Beni comuni';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-rectangle-stack';
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
protected static ?int $navigationSort = 33;
protected static ?string $slug = 'condomini/servizi-utenze';
protected string $view = 'filament.pages.condomini.servizi-stabile-archivio';
public string $acquaTab = 'dashboard';
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore'])
&& Schema::hasTable('stabile_servizi');
}
public function mount(): void
{
$this->mountInteractsWithTable();
}
public function setAcquaTab(string $tab): void
{
$allowed = ['dashboard', 'fatture', 'letture', 'tariffe', 'servizi'];
$this->acquaTab = in_array($tab, $allowed, true) ? $tab : 'dashboard';
}
public function prepareAcquaReadingCampaign(): void
{
$stabileId = $this->resolveActiveStabileId();
$servizio = $this->resolveActiveAcquaServizio();
if (! $stabileId || ! $servizio) {
Notification::make()->title('Servizio acqua attivo non disponibile per lo stabile selezionato.')->warning()->send();
return;
}
$units = UnitaImmobiliare::query()
->where('stabile_id', $stabileId)
->whereNull('deleted_at')
->orderBy('scala')
->orderBy('interno')
->orderBy('id')
->get(['id', 'codice_unita', 'scala', 'interno']);
if ($units->isEmpty()) {
Notification::make()->title('Nessuna unità immobiliare disponibile per avviare la campagna letture.')->warning()->send();
return;
}
$year = $this->resolveActiveAnnoGestione();
$created = 0;
$updated = 0;
foreach ($units as $unit) {
$completed = StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->where('stabile_servizio_id', (int) $servizio->id)
->where('unita_immobiliare_id', (int) $unit->id)
->whereYear('created_at', $year)
->whereNotNull('lettura_fine')
->exists();
if ($completed) {
continue;
}
$previous = StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->where('stabile_servizio_id', (int) $servizio->id)
->where('unita_immobiliare_id', (int) $unit->id)
->whereNotNull('lettura_fine')
->orderByDesc('periodo_al')
->orderByDesc('id')
->first();
$requestRow = StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->where('stabile_servizio_id', (int) $servizio->id)
->where('unita_immobiliare_id', (int) $unit->id)
->whereYear('created_at', $year)
->whereNull('lettura_fine')
->orderByDesc('id')
->first();
$requestReference = 'PUBREQ:' . (int) $servizio->id . ':' . (int) $unit->id . ':' . $year;
$requestPayload = [
'stabile_id' => $stabileId,
'stabile_servizio_id' => (int) $servizio->id,
'unita_immobiliare_id' => (int) $unit->id,
'fornitore_id' => $servizio->fornitore_id,
'periodo_dal' => $previous?->periodo_al,
'periodo_al' => null,
'tipologia_lettura' => 'richiesta_autolettura',
'canale_acquisizione' => 'portale_pubblico',
'riferimento_acquisizione' => $requestReference,
'workflow_stato' => 'richiesta_inviata',
'richiesta_lettura_inviata_at' => now(),
'deadline_lettura_at' => now()->addDays(7),
'lettura_precedente_valore' => $previous?->lettura_fine,
'lettura_inizio' => $previous?->lettura_fine,
'raw' => [
'source' => 'campagna_letture_acqua',
'year' => $year,
'unit_label' => trim((string) ($unit->codice_unita ?? '') . ' ' . (string) ($unit->interno ?? '')),
],
];
if ($requestRow) {
$requestRow->fill($requestPayload);
$requestRow->save();
$updated++;
continue;
}
StabileServizioLettura::query()->create($requestPayload);
$created++;
}
Notification::make()
->title('Campagna letture acqua preparata')
->body("Richieste create: {$created} · richieste aggiornate: {$updated}")
->success()
->send();
}
public function markAcquaReadingReminder(int $readingId): void
{
$reading = StabileServizioLettura::query()->find($readingId);
if (! $reading) {
return;
}
$reading->fill([
'sollecito_inviato_at' => now(),
'workflow_stato' => 'sollecito_inviato',
]);
$reading->save();
Notification::make()->title('Sollecito registrato')->success()->send();
}
public function ensureAcquaAceaContract(): void
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
Notification::make()->title('Stabile attivo non disponibile.')->warning()->send();
return;
}
$fornitoreAcea = Fornitore::query()
->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%ato2%'])
->orderBy('ragione_sociale')
->first() ?? Fornitore::query()
->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%'])
->orderBy('ragione_sociale')
->first();
if (! $fornitoreAcea) {
Notification::make()->title('Fornitore Acea Ato2 non trovato in anagrafica fornitori.')->warning()->send();
return;
}
$existing = StabileServizio::query()
->where('stabile_id', $stabileId)
->where('tipo', 'acqua')
->orderByDesc('attivo')
->orderBy('id')
->first();
if ($existing) {
$existing->fornitore_id = (int) $fornitoreAcea->id;
if (trim((string) ($existing->nome ?? '')) === '') {
$existing->nome = 'Utenza acqua - Acea Ato2';
}
if (! $existing->attivo) {
$existing->attivo = true;
}
$existing->save();
Notification::make()->title('Contratto ACQUA aggiornato su fornitore Acea Ato2.')->success()->send();
return;
}
StabileServizio::query()->create([
'stabile_id' => $stabileId,
'tipo' => 'acqua',
'nome' => 'Utenza acqua - Acea Ato2',
'fornitore_id' => (int) $fornitoreAcea->id,
'attivo' => true,
'meta' => [
'canale_acquisizione' => 'manuale',
],
]);
Notification::make()->title('Contratto ACQUA Acea Ato2 creato.')->success()->send();
}
protected function getTableQuery(): Builder
{
$user = Auth::user();
if (! $user instanceof User) {
return StabileServizio::query()->whereRaw('1 = 0');
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return StabileServizio::query()->whereRaw('1 = 0');
}
return StabileServizio::query()
->where('stabile_id', (int) $stabileId)
->with([
'fornitore:id,ragione_sociale',
'voceSpesa:id,descrizione,tipo_gestione',
])
->withCount('letture')
->orderBy('tipo')
->orderBy('nome')
->orderByDesc('id');
}
public function table(Table $table): Table
{
$tipoOptions = $this->getServiceTypeOptions();
$commonScopeOptions = $this->getCommonAssetScopeOptions();
$allocationOptions = $this->getAllocationScopeOptions();
$fiscalScopeOptions = $this->getFiscalScopeOptions();
$counterScopeOptions = $this->getCounterScopeOptions();
$gestioneOptions = [
'ordinaria' => 'Ordinaria',
'riscaldamento' => 'Riscaldamento',
'straordinaria' => 'Straordinaria',
];
return $table
->striped()
->filters([
SelectFilter::make('tipo')
->label('Tipo servizio')
->options($tipoOptions),
SelectFilter::make('common_asset_scope')
->label('Ambito bene comune')
->options($commonScopeOptions)
->query(function (Builder $query, array $data): Builder {
$value = trim((string) ($data['value'] ?? ''));
if ($value === '') {
return $query;
}
return $query->where('meta->common_asset_scope', $value);
}),
SelectFilter::make('counter_scope')
->label('Tipo contatore')
->options($counterScopeOptions)
->query(function (Builder $query, array $data): Builder {
$value = trim((string) ($data['value'] ?? ''));
if ($value === '') {
return $query;
}
return $query->where('meta->counter_scope', $value);
}),
SelectFilter::make('tipo_gestione')
->label('Gestione (O/R/S)')
->options($gestioneOptions)
->query(function (Builder $query, array $data): Builder {
$value = (string) ($data['value'] ?? '');
if ($value === '') {
return $query;
}
return $query->whereHas('voceSpesa', fn(Builder $q) => $q->where('tipo_gestione', $value));
}),
])
->columns([
TextColumn::make('tipo')
->label('Tipo')
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($tipoOptions, $state))
->sortable(),
TextColumn::make('nome')->label('Nome')->searchable()->wrap()->sortable(),
TextColumn::make('meta.common_asset_label')
->label('Bene / locale')
->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—')
->searchable()
->wrap()
->toggleable(),
TextColumn::make('meta.common_asset_scope')
->label('Ambito')
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($commonScopeOptions, $state))
->toggleable(),
TextColumn::make('meta.allocation_scope')
->label('Addebito')
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($allocationOptions, $state))
->wrap()
->toggleable(),
TextColumn::make('meta.fiscal_scope')
->label('Trattamento')
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($fiscalScopeOptions, $state))
->wrap()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('fornitore.ragione_sociale')->label('Fornitore')->searchable()->wrap()->toggleable(),
TextColumn::make('voceSpesa.descrizione')->label('Voce spesa')->searchable()->wrap()->toggleable(),
TextColumn::make('voceSpesa.tipo_gestione')
->label('Gestione')
->formatStateUsing(fn($state) => $gestioneOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—'))
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('letture_count')->label('Letture')->sortable(),
TextColumn::make('meta.canale_acquisizione')
->label('Canale')
->formatStateUsing(function ($state): string {
$v = strtolower(trim((string) $state));
return match ($v) {
'manuale' => 'Manuale',
'email' => 'Email',
'whatsapp' => 'WhatsApp',
'import_file' => 'Import file',
'm_bus', 'mbus' => 'Contatore elettronico',
default => $v !== '' ? strtoupper($v) : '—',
};
})
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('meta.email_raccolta_letture')->label('Email raccolta')->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('meta.whatsapp_raccolta_letture')->label('WhatsApp raccolta')->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('meta.costo_lettura_unitario')->label('Costo lettura')->money('EUR')->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('meta.costo_ripartizione')->label('Costo ripartizione')->money('EUR')->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('codice_utenza')->label('Utenza')->searchable()->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('codice_cliente')->label('Cliente')->searchable()->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('codice_contratto')->label('Contratto')->searchable()->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('contatore_matricola')->label('Matricola')->searchable()->toggleable(),
TextColumn::make('meta.counter_scope')
->label('Contatore')
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($counterScopeOptions, $state))
->toggleable(),
IconColumn::make('meta.has_dedicated_meter')
->label('Ded.')
->boolean()
->toggleable(isToggledHiddenByDefault: true),
TextColumn::make('meta.served_area_label')
->label('Area servita')
->wrap()
->toggleable(isToggledHiddenByDefault: true),
IconColumn::make('attivo')->label('Attivo')->boolean()->sortable(),
TextColumn::make('updated_at')->label('Aggiornato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true),
])
->headerActions([
Action::make('create')
->label('Nuovo')
->icon('heroicon-o-plus')
->form([
Select::make('tipo')
->label('Tipo servizio')
->options($tipoOptions)
->required(),
TextInput::make('nome')->label('Nome')->maxLength(255),
TextInput::make('common_asset_label')->label('Bene comune / locale / servizio')->maxLength(255),
Select::make('common_asset_scope')
->label('Ambito bene comune')
->options($commonScopeOptions),
Select::make('allocation_scope')
->label('Ripartizione / addebito')
->options($allocationOptions),
Select::make('fiscal_scope')
->label('Trattamento economico / fiscale')
->options($fiscalScopeOptions),
Select::make('fornitore_id')
->label('Fornitore')
->searchable()
->options(fn() => $this->getFornitoriOptions()),
Select::make('voce_spesa_id')
->label('Voce spesa')
->searchable()
->options(fn() => $this->getVociSpesaOptions()),
TextInput::make('codice_utenza')->label('Codice utenza')->maxLength(255),
TextInput::make('codice_cliente')->label('Codice cliente')->maxLength(255),
TextInput::make('codice_contratto')->label('Codice contratto')->maxLength(255),
TextInput::make('contatore_matricola')->label('Matricola contatore')->maxLength(255),
Toggle::make('has_dedicated_meter')->label('Contatore dedicato')->default(false),
Select::make('counter_scope')
->label('Tipo contatore')
->options($counterScopeOptions)
->default('assente'),
TextInput::make('served_area_label')->label('Area servita / utilizzo')->maxLength(255),
Select::make('canale_acquisizione')
->label('Canale acquisizione letture')
->options([
'manuale' => 'Manuale',
'email' => 'Email',
'whatsapp' => 'WhatsApp',
'import_file' => 'Import file',
'm_bus' => 'Contatore elettronico',
]),
TextInput::make('email_raccolta_letture')->label('Email raccolta letture')->email()->maxLength(255),
TextInput::make('whatsapp_raccolta_letture')->label('Numero WhatsApp raccolta')->maxLength(50),
TextInput::make('costo_lettura_unitario')->label('Costo lettura (unitario)')->numeric(),
TextInput::make('costo_ripartizione')->label('Costo ripartizione')->numeric(),
Toggle::make('ripartizione_esterna')->label('Ripartizione affidata a ditta esterna')->default(false),
Textarea::make('operative_notes')->label('Note operative')->rows(3)->maxLength(2000),
Toggle::make('attivo')->label('Attivo')->default(true),
])
->action(function (array $data): void {
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return;
}
StabileServizio::query()->create([
'stabile_id' => (int) $stabileId,
'tipo' => strtolower(trim((string) ($data['tipo'] ?? ''))),
'nome' => $this->resolveServiceDisplayName($data),
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null,
'codice_utenza' => trim((string) ($data['codice_utenza'] ?? '')) ?: null,
'codice_cliente' => trim((string) ($data['codice_cliente'] ?? '')) ?: null,
'codice_contratto' => trim((string) ($data['codice_contratto'] ?? '')) ?: null,
'contatore_matricola' => trim((string) ($data['contatore_matricola'] ?? '')) ?: null,
'attivo' => (bool) ($data['attivo'] ?? true),
'meta' => $this->buildServiceMetaPayload([], $data),
]);
}),
])
->actions([
Action::make('letture')
->label('Letture')
->icon('heroicon-o-clipboard-document-list')
->url(fn(StabileServizio $record): string => LettureServiziArchivio::getUrl(['servizio' => (int) $record->id], panel: 'admin-filament')),
Action::make('fatture')
->label('Fatture')
->icon('heroicon-o-document-text')
->url(fn() => FattureElettronicheP7mRicevute::getUrl(panel: 'admin-filament')),
Action::make('edit')
->label('Modifica')
->icon('heroicon-o-pencil-square')
->form([
Select::make('tipo')
->label('Tipo servizio')
->options($tipoOptions)
->required(),
TextInput::make('nome')->label('Nome')->maxLength(255),
TextInput::make('common_asset_label')->label('Bene comune / locale / servizio')->maxLength(255),
Select::make('common_asset_scope')
->label('Ambito bene comune')
->options($commonScopeOptions),
Select::make('allocation_scope')
->label('Ripartizione / addebito')
->options($allocationOptions),
Select::make('fiscal_scope')
->label('Trattamento economico / fiscale')
->options($fiscalScopeOptions),
Select::make('fornitore_id')
->label('Fornitore')
->searchable()
->options(fn() => $this->getFornitoriOptions()),
Select::make('voce_spesa_id')
->label('Voce spesa')
->searchable()
->options(fn() => $this->getVociSpesaOptions()),
TextInput::make('codice_utenza')->label('Codice utenza')->maxLength(255),
TextInput::make('codice_cliente')->label('Codice cliente')->maxLength(255),
TextInput::make('codice_contratto')->label('Codice contratto')->maxLength(255),
TextInput::make('contatore_matricola')->label('Matricola contatore')->maxLength(255),
Toggle::make('has_dedicated_meter')->label('Contatore dedicato')->default(false),
Select::make('counter_scope')
->label('Tipo contatore')
->options($counterScopeOptions),
TextInput::make('served_area_label')->label('Area servita / utilizzo')->maxLength(255),
Select::make('canale_acquisizione')
->label('Canale acquisizione letture')
->options([
'manuale' => 'Manuale',
'email' => 'Email',
'whatsapp' => 'WhatsApp',
'import_file' => 'Import file',
'm_bus' => 'Contatore elettronico',
]),
TextInput::make('email_raccolta_letture')->label('Email raccolta letture')->email()->maxLength(255),
TextInput::make('whatsapp_raccolta_letture')->label('Numero WhatsApp raccolta')->maxLength(50),
TextInput::make('costo_lettura_unitario')->label('Costo lettura (unitario)')->numeric(),
TextInput::make('costo_ripartizione')->label('Costo ripartizione')->numeric(),
Toggle::make('ripartizione_esterna')->label('Ripartizione affidata a ditta esterna')->default(false),
Textarea::make('operative_notes')->label('Note operative')->rows(3)->maxLength(2000),
Toggle::make('attivo')->label('Attivo')->default(true),
])
->fillForm(fn(StabileServizio $record): array=> [
'tipo' => (string) ($record->tipo ?? ''),
'nome' => (string) ($record->nome ?? ''),
'common_asset_label' => (string) (($record->meta['common_asset_label'] ?? '') ?: ''),
'common_asset_scope' => (string) (($record->meta['common_asset_scope'] ?? '') ?: ''),
'allocation_scope' => (string) (($record->meta['allocation_scope'] ?? '') ?: ''),
'fiscal_scope' => (string) (($record->meta['fiscal_scope'] ?? '') ?: ''),
'fornitore_id' => $record->fornitore_id,
'voce_spesa_id' => $record->voce_spesa_id,
'codice_utenza' => (string) ($record->codice_utenza ?? ''),
'codice_cliente' => (string) ($record->codice_cliente ?? ''),
'codice_contratto' => (string) ($record->codice_contratto ?? ''),
'contatore_matricola' => (string) ($record->contatore_matricola ?? ''),
'has_dedicated_meter' => (bool) ($record->meta['has_dedicated_meter'] ?? false),
'counter_scope' => (string) (($record->meta['counter_scope'] ?? '') ?: ''),
'served_area_label' => (string) (($record->meta['served_area_label'] ?? '') ?: ''),
'canale_acquisizione' => (string) (($record->meta['canale_acquisizione'] ?? '') ?: ''),
'email_raccolta_letture' => (string) (($record->meta['email_raccolta_letture'] ?? '') ?: ''),
'whatsapp_raccolta_letture' => (string) (($record->meta['whatsapp_raccolta_letture'] ?? '') ?: ''),
'costo_lettura_unitario' => $record->meta['costo_lettura_unitario'] ?? null,
'costo_ripartizione' => $record->meta['costo_ripartizione'] ?? null,
'ripartizione_esterna' => (bool) ($record->meta['ripartizione_esterna'] ?? false),
'operative_notes' => (string) (($record->meta['operative_notes'] ?? '') ?: ''),
'attivo' => (bool) ($record->attivo ?? true),
])
->action(function (StabileServizio $record, array $data): void {
$meta = $this->buildServiceMetaPayload(is_array($record->meta ?? null) ? $record->meta : [], $data);
$record->fill([
'tipo' => strtolower(trim((string) ($data['tipo'] ?? ''))),
'nome' => $this->resolveServiceDisplayName($data, (string) ($record->nome ?? '')),
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null,
'codice_utenza' => trim((string) ($data['codice_utenza'] ?? '')) ?: null,
'codice_cliente' => trim((string) ($data['codice_cliente'] ?? '')) ?: null,
'codice_contratto' => trim((string) ($data['codice_contratto'] ?? '')) ?: null,
'contatore_matricola' => trim((string) ($data['contatore_matricola'] ?? '')) ?: null,
'attivo' => (bool) ($data['attivo'] ?? true),
'meta' => $meta,
]);
$record->save();
}),
Action::make('delete')
->label('Elimina')
->icon('heroicon-o-trash')
->color('danger')
->requiresConfirmation()
->action(fn(StabileServizio $record) => $record->delete()),
]);
}
/**
* @return array<string, string>
*/
private function getServiceTypeOptions(): array
{
return [
'acqua' => 'Acqua',
'energia' => 'Energia',
'gas' => 'Gas',
'riscaldamento' => 'Riscaldamento',
'ascensore' => 'Ascensore',
'pulizie' => 'Pulizie',
'assicurazione' => 'Assicurazione',
'privacy' => 'Privacy',
'antenna' => 'Antenna',
'antincendio' => 'Antincendio',
'citofonia' => 'Citofonia',
'locale_comune' => 'Locale comune',
'spazio_comune' => 'Spazio / area comune',
'impianto' => 'Impianto comune',
'altro' => 'Altro',
];
}
/**
* @return array<string, string>
*/
private function getCommonAssetScopeOptions(): array
{
return [
'locale' => 'Locale',
'spazio' => 'Spazio comune',
'impianto' => 'Impianto',
'area_esterna' => 'Area esterna / giardino',
'copertura' => 'Terrazzo / copertura',
'accesso' => 'Portone / accesso',
'servizio' => 'Servizio comune',
'sicurezza' => 'Sicurezza / antincendio',
'amministrativo'=> 'Privacy / assicurazione / studio',
'altro' => 'Altro',
];
}
/**
* @return array<string, string>
*/
private function getAllocationScopeOptions(): array
{
return [
'tutti_millesimi' => 'Tutti per millesimi',
'condomini_utilizzatori' => 'Condomini utilizzatori',
'inquilini_utilizzatori' => 'Inquilini utilizzatori',
'condomini_inquilini_utilizzatori'=> 'Condomini + inquilini utilizzatori',
'rimborso_spese' => 'Rimborso spese uso bene comune',
'gestione_studio' => 'Gestione studio / amministrazione',
'altro' => 'Altro criterio',
];
}
/**
* @return array<string, string>
*/
private function getFiscalScopeOptions(): array
{
return [
'spesa_condominiale' => 'Spesa condominiale',
'rimborso_non_fiscale' => 'Rimborso spese non fiscale',
'da_fatturare' => 'Da fatturare',
'polizza_o_servizio' => 'Polizza / servizio',
'non_applicabile' => 'Non applicabile',
];
}
/**
* @return array<string, string>
*/
private function getCounterScopeOptions(): array
{
return [
'generale' => 'Generale',
'particolare' => 'Particolare / dedicato',
'assente' => 'Senza contatore',
];
}
private function formatMetaOptionLabel(array $options, mixed $state): string
{
$value = strtolower(trim((string) $state));
return $options[$value] ?? ($value !== '' ? (string) $state : '—');
}
/**
* @param array<string, mixed> $currentMeta
* @param array<string, mixed> $data
* @return array<string, mixed>
*/
private function buildServiceMetaPayload(array $currentMeta, array $data): array
{
$meta = $currentMeta;
$meta['common_asset_label'] = trim((string) ($data['common_asset_label'] ?? '')) ?: null;
$meta['common_asset_scope'] = trim((string) ($data['common_asset_scope'] ?? '')) ?: null;
$meta['allocation_scope'] = trim((string) ($data['allocation_scope'] ?? '')) ?: null;
$meta['fiscal_scope'] = trim((string) ($data['fiscal_scope'] ?? '')) ?: null;
$meta['has_dedicated_meter'] = (bool) ($data['has_dedicated_meter'] ?? false);
$meta['counter_scope'] = trim((string) ($data['counter_scope'] ?? '')) ?: (($meta['has_dedicated_meter'] ?? false) ? 'particolare' : 'assente');
$meta['served_area_label'] = trim((string) ($data['served_area_label'] ?? '')) ?: null;
$meta['canale_acquisizione'] = trim((string) ($data['canale_acquisizione'] ?? '')) ?: null;
$meta['email_raccolta_letture'] = trim((string) ($data['email_raccolta_letture'] ?? '')) ?: null;
$meta['whatsapp_raccolta_letture'] = trim((string) ($data['whatsapp_raccolta_letture'] ?? '')) ?: null;
$meta['costo_lettura_unitario'] = is_numeric($data['costo_lettura_unitario'] ?? null) ? (float) $data['costo_lettura_unitario'] : null;
$meta['costo_ripartizione'] = is_numeric($data['costo_ripartizione'] ?? null) ? (float) $data['costo_ripartizione'] : null;
$meta['ripartizione_esterna'] = (bool) ($data['ripartizione_esterna'] ?? false);
$meta['operative_notes'] = trim((string) ($data['operative_notes'] ?? '')) ?: null;
return $meta;
}
/**
* @param array<string, mixed> $data
*/
private function resolveServiceDisplayName(array $data, string $fallback = ''): string
{
$name = trim((string) ($data['nome'] ?? ''));
if ($name !== '') {
return $name;
}
$commonLabel = trim((string) ($data['common_asset_label'] ?? ''));
if ($commonLabel !== '') {
return $commonLabel;
}
return trim($fallback);
}
/**
* @return array<int, string>
*/
private function getFornitoriOptions(): array
{
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$query = Fornitore::query();
if (! $user->hasAnyRole(['super-admin', 'admin'])) {
$activeStabile = StabileContext::getActiveStabile($user);
$adminId = (int) ($activeStabile?->amministratore_id ?: 0);
if ($adminId > 0) {
$query->where('amministratore_id', $adminId);
}
}
return $query
->orderBy('ragione_sociale')
->get(['id', 'ragione_sociale'])
->mapWithKeys(fn(Fornitore $f) => [(int) $f->id => (string) ($f->ragione_sociale ?? ('Fornitore #' . $f->id))])
->all();
}
/**
* @return array<int, string>
*/
private function getVociSpesaOptions(): array
{
$user = Auth::user();
if (! $user instanceof User) {
return [];
}
$stabileId = StabileContext::resolveActiveStabileId($user);
if (! $stabileId) {
return [];
}
return VoceSpesa::query()
->where('stabile_id', (int) $stabileId)
->orderBy('tipo_gestione')
->orderBy('descrizione')
->get(['id', 'descrizione', 'tipo_gestione'])
->mapWithKeys(function (VoceSpesa $v): array {
$tipo = strtolower(trim((string) ($v->tipo_gestione ?? '')));
$prefix = $tipo !== '' ? strtoupper(substr($tipo, 0, 1)) . ' - ' : '';
return [(int) $v->id => $prefix . (string) ($v->descrizione ?? ('Voce #' . $v->id))];
})
->all();
}
private function resolveActiveStabileId(): ?int
{
$user = Auth::user();
if (! $user instanceof User) {
return null;
}
$stabileId = StabileContext::resolveActiveStabileId($user);
return $stabileId ? (int) $stabileId : null;
}
private function resolveActiveAnnoGestione(): int
{
$user = Auth::user();
return AnnoGestioneContext::resolveActiveAnno($user instanceof User ? $user : null);
}
private function resolveActiveAcquaServizio(): ?StabileServizio
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return null;
}
return StabileServizio::query()
->where('stabile_id', $stabileId)
->where('tipo', 'acqua')
->orderByDesc('attivo')
->orderBy('id')
->first();
}
private function buildPublicAcquaReadingUrl(int $servizioId, int $unitaId, ?int $requestId = null): string
{
return URL::temporarySignedRoute(
'public.water-reading.show',
now()->addDays(30),
array_filter([
'servizio' => $servizioId,
'unita' => $unitaId,
'request' => $requestId,
], fn($value) => $value !== null)
);
}
/** @return array<int, string> */
private function resolveLegacyCodiciStabile(): array
{
$user = Auth::user();
return StabileContext::legacyCodeCandidates(StabileContext::getActiveStabile($user instanceof User ? $user : null));
}
private function applyActiveYearFilterToReadingQuery(Builder $query): Builder
{
$year = $this->resolveActiveAnnoGestione();
return $query->where(function (Builder $inner) use ($year): void {
$inner->whereYear('periodo_al', $year)
->orWhere(function (Builder $fallback) use ($year): void {
$fallback->whereNull('periodo_al')
->whereYear('periodo_dal', $year);
})
->orWhere(function (Builder $fallback) use ($year): void {
$fallback->whereNull('periodo_al')
->whereNull('periodo_dal')
->whereYear('created_at', $year);
});
});
}
/** @return array{totale_fatture: float, totale_operazioni_ac12_legacy: float, delta_operazioni_vs_fatture: float, totale_letture: int, totale_letture_con_riferimento: int, totale_consumi_mc: float, totale_tariffe: int} */
public function getAcquaDashboardStatsProperty(): array
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return [
'totale_fatture' => 0.0,
'totale_operazioni_ac12_legacy' => 0.0,
'delta_operazioni_vs_fatture' => 0.0,
'totale_letture' => 0,
'totale_letture_con_riferimento' => 0,
'totale_consumi_mc' => 0.0,
'totale_tariffe' => 0,
];
}
$fattureRows = $this->acquaFatturePerGestione;
$totaleFatture = (float) collect($fattureRows)->sum(fn(array $r): float => (float) ($r['totale_fatture'] ?? 0));
$totaleLetture = (int) StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua'))
->count();
$totaleLettureConRiferimento = (int) StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua'))
->whereNotNull('riferimento_acquisizione')
->where('riferimento_acquisizione', '!=', '')
->count();
$totaleConsumi = (float) StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua'))
->sum('consumo_valore');
$totaleTariffe = (int) StabileServizioTariffa::query()
->where('stabile_id', $stabileId)
->count();
$legacySummary = $this->acquaLegacyOperazioniSummary;
$totaleLegacyAc12 = (float) ($legacySummary['totale'] ?? 0);
return [
'totale_fatture' => round($totaleFatture, 2),
'totale_operazioni_ac12_legacy' => round($totaleLegacyAc12, 2),
'delta_operazioni_vs_fatture' => round($totaleFatture - $totaleLegacyAc12, 2),
'totale_letture' => $totaleLetture,
'totale_letture_con_riferimento' => $totaleLettureConRiferimento,
'totale_consumi_mc' => round($totaleConsumi, 3),
'totale_tariffe' => $totaleTariffe,
];
}
private function normalizeInvoiceNumber(mixed $number): ?string
{
if ($number === null) {
return null;
}
$raw = strtoupper(trim((string) $number));
if ($raw === '') {
return null;
}
$normalized = preg_replace('/[^A-Z0-9]/', '', $raw);
return $normalized !== '' ? $normalized : null;
}
private function parseLegacyDate(mixed $value): ?string
{
if ($value === null || $value === '') {
return null;
}
try {
return (string) \Carbon\Carbon::parse((string) $value)->format('Y-m-d');
} catch (\Throwable) {
return null;
}
}
/** @return array{totale: float, voci: array<string, float>, righe: int} */
public function getAcquaLegacyOperazioniSummaryProperty(): array
{
if (! Schema::connection('gescon_import')->hasTable('operazioni')) {
return ['totale' => 0.0, 'voci' => [], 'righe' => 0];
}
$legacyCodes = $this->resolveLegacyCodiciStabile();
$year = $this->resolveActiveAnnoGestione();
$query = DB::connection('gescon_import')
->table('operazioni')
->where('gestione', 'O')
->whereIn('cod_spe', ['AC1', 'AC2']);
if ($legacyCodes !== [] && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) {
$query->whereIn('cod_stabile', $legacyCodes);
}
if (Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe')) {
$query->whereYear('dt_spe', $year);
}
$rows = $query
->select('cod_spe')
->selectRaw('COUNT(*) as righe')
->selectRaw('SUM(COALESCE(importo_euro, importo, 0)) as totale')
->groupBy('cod_spe')
->get();
$voci = [];
$totale = 0.0;
$righe = 0;
foreach ($rows as $row) {
$cod = strtoupper(trim((string) ($row->cod_spe ?? '')));
if ($cod === '') {
continue;
}
$imp = (float) ($row->totale ?? 0);
$voci[$cod] = round($imp, 2);
$totale += $imp;
$righe += (int) ($row->righe ?? 0);
}
return [
'totale' => round($totale, 2),
'voci' => $voci,
'righe' => $righe,
];
}
/** @return array<int, array<string,mixed>> */
public function getAcquaLegacyOperazioniRowsProperty(): array
{
if (! Schema::connection('gescon_import')->hasTable('operazioni')) {
return [];
}
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return [];
}
$legacyCodes = $this->resolveLegacyCodiciStabile();
$year = $this->resolveActiveAnnoGestione();
$query = DB::connection('gescon_import')
->table('operazioni')
->where('gestione', 'O')
->whereIn('cod_spe', ['AC1', 'AC2']);
if ($legacyCodes !== [] && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) {
$query->whereIn('cod_stabile', $legacyCodes);
}
if (Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe')) {
$query->whereYear('dt_spe', $year);
}
$legacyRows = $query
->orderByDesc('dt_spe')
->orderByDesc('id_operaz')
->limit(120)
->get([
'id_operaz',
'dt_spe',
'cod_spe',
'cod_for',
'num_fat',
'dt_fat',
'benef',
'importo_euro',
'importo',
'fe_uid',
]);
if ($legacyRows->isEmpty()) {
return [];
}
$codForList = $legacyRows
->pluck('cod_for')
->filter()
->map(fn($v) => trim((string) $v))
->filter()
->unique()
->values()
->all();
$codForNoZero = array_values(array_filter(array_unique(array_map(fn($v) => ltrim((string) $v, '0'), $codForList))));
$codForAll = array_values(array_unique(array_merge($codForList, $codForNoZero)));
$legacyFornitoriMap = [];
$codFornToLegacyId = [];
if (! empty($codForAll)) {
$useStg = Schema::hasTable('stg_fornitori_gescon');
$fornRows = $useStg
? DB::table('stg_fornitori_gescon')
->whereIn('cod_forn', $codForAll)
->get(['cod_forn', 'legacy_id_fornitore', 'denominazione', 'cognome', 'nome'])
: DB::connection('gescon_import')
->table('mdb_fornitori')
->whereIn('cod_forn', $codForAll)
->get(['cod_forn', 'id_fornitore', 'denominazione', 'cognome', 'nome']);
foreach ($fornRows as $f) {
$code = trim((string) ($f->cod_forn ?? ''));
if ($code === '') {
continue;
}
$label = trim((string) ($f->denominazione ?? ''));
if ($label === '') {
$label = trim((string) (($f->nome ?? '') . ' ' . ($f->cognome ?? '')));
}
$legacyId = $useStg ? ($f->cod_forn ?? null) : ($f->id_fornitore ?? null);
$legacyFornitoriMap[$code] = $label !== '' ? $label : null;
$codFornToLegacyId[$code] = $legacyId;
$noZero = ltrim($code, '0');
if ($noZero !== '') {
$legacyFornitoriMap[$noZero] = $legacyFornitoriMap[$code];
$codFornToLegacyId[$noZero] = $legacyId;
}
}
}
$legacyIds = array_values(array_filter(array_unique(array_values($codFornToLegacyId))));
$fornitoriIdMap = [];
if (! empty($legacyIds) && Schema::hasTable('fornitori')) {
$fornitoriRows = DB::table('fornitori')
->whereIn('old_id', $legacyIds)
->get(['id', 'old_id']);
foreach ($fornitoriRows as $fr) {
$fornitoriIdMap[(string) ($fr->old_id ?? '')] = (int) ($fr->id ?? 0);
}
}
$fattureByFornitore = [];
$fattureByStrict = [];
$fattureByGlobal = [];
if (! empty($fornitoriIdMap) && Schema::hasTable('contabilita_fatture_fornitori')) {
$fornitoreIds = array_values(array_unique(array_filter(array_values($fornitoriIdMap))));
if (! empty($fornitoreIds)) {
$fatture = DB::table('contabilita_fatture_fornitori')
->where('stabile_id', $stabileId)
->whereIn('fornitore_id', $fornitoreIds)
->get([
'id',
'fornitore_id',
'fattura_elettronica_id',
'numero_documento',
'data_documento',
'totale',
'netto_da_pagare',
]);
foreach ($fatture as $f) {
$fid = (int) ($f->fornitore_id ?? 0);
if ($fid <= 0) {
continue;
}
$fattureByFornitore[$fid][] = $f;
$docNumber = $this->normalizeInvoiceNumber($f->numero_documento ?? null);
$docDate = $this->parseLegacyDate($f->data_documento ?? null);
if ($docNumber && $docDate) {
$strictKey = $fid . '|' . $docNumber . '|' . $docDate;
$fattureByStrict[$strictKey][] = $f;
$globalKey = $docNumber . '|' . $docDate;
$fattureByGlobal[$globalKey][] = $f;
}
}
}
}
return $legacyRows->map(function ($row) use ($legacyFornitoriMap, $codFornToLegacyId, $fornitoriIdMap, $fattureByFornitore, $fattureByStrict, $fattureByGlobal): array {
$codFor = trim((string) ($row->cod_for ?? ''));
$codForNoZero = ltrim($codFor, '0');
$legacyId = $codFornToLegacyId[$codFor] ?? ($codForNoZero !== '' ? ($codFornToLegacyId[$codForNoZero] ?? null) : null);
$fornitoreId = ($legacyId !== null && isset($fornitoriIdMap[(string) $legacyId])) ? (int) $fornitoriIdMap[(string) $legacyId] : null;
$fornitoreLegacy = $legacyFornitoriMap[$codFor] ?? ($codForNoZero !== '' ? ($legacyFornitoriMap[$codForNoZero] ?? null) : null);
$numFatNorm = $this->normalizeInvoiceNumber($row->num_fat ?? null);
$dtFatIso = $this->parseLegacyDate($row->dt_fat ?? null);
$importo = (float) ($row->importo_euro ?? $row->importo ?? 0);
$matched = null;
$matchType = 'none';
if ($fornitoreId && $numFatNorm && $dtFatIso) {
$key = $fornitoreId . '|' . $numFatNorm . '|' . $dtFatIso;
if (! empty($fattureByStrict[$key])) {
$matched = $fattureByStrict[$key][0];
$matchType = 'strict';
}
}
if (! $matched && $numFatNorm && $dtFatIso) {
$gKey = $numFatNorm . '|' . $dtFatIso;
$globalMatches = $fattureByGlobal[$gKey] ?? [];
if (count($globalMatches) === 1) {
$matched = $globalMatches[0];
$matchType = 'global';
}
}
if (! $matched && $fornitoreId && ! empty($fattureByFornitore[$fornitoreId])) {
foreach ($fattureByFornitore[$fornitoreId] as $f) {
$tot = (float) ($f->totale ?? 0);
$net = (float) ($f->netto_da_pagare ?? 0);
if (abs(abs($importo) - abs($tot)) <= 0.01 || abs(abs($importo) - abs($net)) <= 0.01) {
$matched = $f;
$matchType = 'amount';
break;
}
}
}
return [
'id_operaz' => (int) ($row->id_operaz ?? 0),
'data_operazione' => $this->parseLegacyDate($row->dt_spe ?? null),
'cod_spe' => strtoupper(trim((string) ($row->cod_spe ?? ''))),
'cod_for' => $codFor,
'fornitore_legacy' => $fornitoreLegacy,
'num_fat' => (string) ($row->num_fat ?? ''),
'dt_fat' => $dtFatIso,
'benef' => (string) ($row->benef ?? ''),
'importo' => round($importo, 2),
'fe_uid' => (string) ($row->fe_uid ?? ''),
'match_type' => $matchType,
'matched_fattura_id' => $matched ? (int) ($matched->id ?? 0) : null,
'matched_fe_id' => $matched ? ((int) ($matched->fattura_elettronica_id ?? 0) ?: null): null,
'matched_numero' => $matched ? (string) ($matched->numero_documento ?? '') : null,
'matched_data' => $matched ? $this->parseLegacyDate($matched->data_documento ?? null) : null,
'matched_totale' => $matched ? (float) ($matched->totale ?? $matched->netto_da_pagare ?? 0) : null,
];
})->all();
}
/** @return array{servizio_id: int|null, servizio_nome: string|null, fornitore_id: int|null, fornitore_nome: string|null, codice_utenza: string|null, codice_cliente: string|null, codice_contratto: string|null, matricola: string|null, suggerito_fornitore: string|null} */
public function getAcquaContrattoStabileProperty(): array
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return [
'servizio_id' => null,
'servizio_nome' => null,
'fornitore_id' => null,
'fornitore_nome' => null,
'codice_utenza' => null,
'codice_cliente' => null,
'codice_contratto' => null,
'matricola' => null,
'suggerito_fornitore' => null,
];
}
$servizio = StabileServizio::query()
->where('stabile_id', $stabileId)
->where('tipo', 'acqua')
->with('fornitore:id,ragione_sociale')
->orderByDesc('attivo')
->orderBy('id')
->first();
$fornitoreSuggerito = Fornitore::query()
->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%ato2%'])
->value('ragione_sociale') ?? Fornitore::query()
->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%'])
->value('ragione_sociale');
return [
'servizio_id' => $servizio?->id,
'servizio_nome' => $servizio?->nome,
'fornitore_id' => $servizio?->fornitore_id,
'fornitore_nome' => $servizio?->fornitore?->ragione_sociale,
'codice_utenza' => $servizio?->codice_utenza,
'codice_cliente' => $servizio?->codice_cliente,
'codice_contratto' => $servizio?->codice_contratto,
'matricola' => $servizio?->contatore_matricola,
'suggerito_fornitore' => $fornitoreSuggerito ? (string) $fornitoreSuggerito : null,
];
}
/** @return array<int, array{id_cond:int|null, ruolo:string, nominativo:string, scala:string, interno:string, quota_euro:float, lettura_iniziale:?float, consumo_mc:?float}> */
public function getAcquaRipartoNominativiLegacyProperty(): array
{
if (! Schema::connection('gescon_import')->hasTable('dett_tab') || ! Schema::connection('gescon_import')->hasTable('condomin')) {
return [];
}
$legacyCodes = $this->resolveLegacyCodiciStabile();
if ($legacyCodes === []) {
return [];
}
$legacyYear = (string) $this->resolveActiveAnnoGestione();
$condQuery = DB::connection('gescon_import')
->table('condomin')
->whereIn('cod_stabile', $legacyCodes);
if ($legacyYear !== '' && Schema::connection('gescon_import')->hasColumn('condomin', 'legacy_year')) {
$condQuery->where('legacy_year', $legacyYear);
}
$condRows = $condQuery->get([
'id_cond',
'scala',
'interno',
'cognome',
'nome',
'proprietario_denominazione',
'inquilino_denominazione',
'inquil_nome',
])
->keyBy('id_cond');
$rowsQuery = DB::connection('gescon_import')
->table('dett_tab')
->where('cod_tab', 'ACQUA')
->whereIn('cod_stabile', $legacyCodes);
if ($legacyYear !== '' && Schema::connection('gescon_import')->hasColumn('dett_tab', 'legacy_year')) {
$rowsQuery->where('legacy_year', $legacyYear);
}
$rows = $rowsQuery
->orderBy('id_cond')
->orderBy('cond_inquil')
->get(['id_cond', 'cond_inquil', 'cons_euro']);
$stabileId = $this->resolveActiveStabileId();
$letturaByUi = [];
if ($stabileId) {
$lettureRows = StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua'))
->whereNotNull('unita_immobiliare_id')
->orderByDesc('created_at')
->get(['unita_immobiliare_id', 'lettura_inizio', 'consumo_valore']);
foreach ($lettureRows as $lr) {
$uiId = (int) ($lr->unita_immobiliare_id ?? 0);
if ($uiId <= 0 || isset($letturaByUi[$uiId])) {
continue;
}
$letturaByUi[$uiId] = [
'lettura_iniziale' => is_numeric($lr->lettura_inizio) ? (float) $lr->lettura_inizio : null,
'consumo_mc' => is_numeric($lr->consumo_valore) ? (float) $lr->consumo_valore : null,
];
}
}
$uiByLegacyCond = [];
$uiByScalaInt = [];
if ($stabileId && Schema::hasTable('unita_immobiliari')) {
$uiRows = DB::table('unita_immobiliari')
->where('stabile_id', $stabileId)
->get(['id', 'legacy_cond_id', 'scala', 'interno']);
foreach ($uiRows as $ui) {
$uiId = (int) ($ui->id ?? 0);
if ($uiId <= 0) {
continue;
}
if (is_numeric($ui->legacy_cond_id ?? null)) {
$uiByLegacyCond[(int) $ui->legacy_cond_id] = $uiId;
}
$scala = strtoupper(trim((string) ($ui->scala ?? '')));
$interno = strtoupper(trim((string) ($ui->interno ?? '')));
if ($scala !== '' || $interno !== '') {
$uiByScalaInt[$scala . '|' . $interno] = $uiId;
}
}
}
return $rows->map(function ($r) use ($condRows, $uiByLegacyCond, $uiByScalaInt, $letturaByUi): ?array {
$cond = $condRows[$r->id_cond] ?? null;
$ruolo = strtoupper(trim((string) ($r->cond_inquil ?? '')));
$nominativo = '';
if ($cond) {
if ($ruolo === 'I') {
$nominativo = trim((string) ($cond->inquilino_denominazione ?? ''));
if ($nominativo === '') {
$nominativo = trim((string) ($cond->inquil_nome ?? ''));
}
} else {
$nominativo = trim((string) ($cond->proprietario_denominazione ?? ''));
if ($nominativo === '') {
$nominativo = trim((string) (($cond->cognome ?? '') . ' ' . ($cond->nome ?? '')));
}
}
}
if ($nominativo === '') {
return null;
}
$uiId = null;
if (is_numeric($r->id_cond ?? null)) {
$uiId = $uiByLegacyCond[(int) $r->id_cond] ?? null;
}
if (! $uiId) {
$scala = strtoupper(trim((string) ($cond->scala ?? '')));
$interno = strtoupper(trim((string) ($cond->interno ?? '')));
$uiId = $uiByScalaInt[$scala . '|' . $interno] ?? null;
}
$lettura = ($uiId && isset($letturaByUi[$uiId]))
? $letturaByUi[$uiId]
: ['lettura_iniziale' => null, 'consumo_mc' => null];
return [
'id_cond' => is_numeric($r->id_cond ?? null) ? (int) $r->id_cond : null,
'ruolo' => $ruolo !== '' ? $ruolo : 'C',
'nominativo' => $nominativo,
'scala' => trim((string) ($cond->scala ?? '')),
'interno' => trim((string) ($cond->interno ?? '')),
'quota_euro' => round((float) ($r->cons_euro ?? 0), 2),
'lettura_iniziale' => isset($lettura['lettura_iniziale']) && is_numeric($lettura['lettura_iniziale']) ? (float) $lettura['lettura_iniziale'] : null,
'consumo_mc' => isset($lettura['consumo_mc']) && is_numeric($lettura['consumo_mc']) ? (float) $lettura['consumo_mc'] : null,
];
})->filter()->values()->all();
}
/** @return array<int, array{gestione: string, fatture_count: int, totale_fatture: float}> */
public function getAcquaFatturePerGestioneProperty(): array
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return [];
}
$year = $this->resolveActiveAnnoGestione();
if (Schema::hasTable('contabilita_fatture_fornitori')) {
$fornitoreIds = StabileServizio::query()
->where('stabile_id', $stabileId)
->where('tipo', 'acqua')
->whereNotNull('fornitore_id')
->pluck('fornitore_id')
->map(fn($v) => (int) $v)
->filter(fn($v) => $v > 0)
->unique()
->values()
->all();
if ($fornitoreIds !== []) {
$contabili = DB::table('contabilita_fatture_fornitori as f')
->leftJoin('gestioni_contabili as g', 'g.id', '=', 'f.gestione_id')
->where('f.stabile_id', $stabileId)
->whereIn('f.fornitore_id', $fornitoreIds)
->when(Schema::hasColumn('gestioni_contabili', 'anno_gestione'), fn($q) => $q->where('g.anno_gestione', $year))
->when(! Schema::hasColumn('gestioni_contabili', 'anno_gestione') && Schema::hasColumn('contabilita_fatture_fornitori', 'data_documento'), fn($q) => $q->whereYear('f.data_documento', $year))
->get(['f.id', 'f.totale', 'f.fattura_elettronica_id', 'g.tipo_gestione']);
if ($contabili->isNotEmpty()) {
$grouped = [];
foreach ($contabili as $row) {
$g = strtolower(trim((string) ($row->tipo_gestione ?? '')));
if ($g === '') {
$g = 'non definita';
}
if (! isset($grouped[$g])) {
$grouped[$g] = [
'gestione' => $g,
'fatture_count' => 0,
'totale_fatture' => 0.0,
'fe_count' => 0,
'manual_count' => 0,
];
}
$grouped[$g]['fatture_count']++;
$grouped[$g]['totale_fatture'] += (float) ($row->totale ?? 0);
if (is_numeric($row->fattura_elettronica_id ?? null) && (int) $row->fattura_elettronica_id > 0) {
$grouped[$g]['fe_count']++;
} else {
$grouped[$g]['manual_count']++;
}
}
$out = array_values($grouped);
usort($out, fn($a, $b) => strcmp((string) $a['gestione'], (string) $b['gestione']));
foreach ($out as &$row) {
$row['totale_fatture'] = round((float) $row['totale_fatture'], 2);
}
return $out;
}
}
}
$rows = StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua'))
->whereNotNull('fattura_elettronica_id')
->with(['voceSpesa:id,tipo_gestione'])
->tap(fn(Builder $query) => $this->applyActiveYearFilterToReadingQuery($query))
->get(['fattura_elettronica_id', 'voce_spesa_id', 'importo_totale']);
$byFattura = [];
foreach ($rows as $row) {
$fatturaId = (int) ($row->fattura_elettronica_id ?? 0);
if ($fatturaId <= 0) {
continue;
}
if (! isset($byFattura[$fatturaId])) {
$byFattura[$fatturaId] = [
'gestione' => 'non definita',
'importo' => 0.0,
];
}
$tipoGestione = strtolower(trim((string) ($row->voceSpesa?->tipo_gestione ?? '')));
if ($tipoGestione !== '') {
$byFattura[$fatturaId]['gestione'] = $tipoGestione;
}
$imp = is_numeric($row->importo_totale) ? (float) $row->importo_totale : 0.0;
if ($imp > (float) $byFattura[$fatturaId]['importo']) {
$byFattura[$fatturaId]['importo'] = $imp;
}
}
$grouped = [];
foreach ($byFattura as $item) {
$g = (string) ($item['gestione'] ?? 'non definita');
if (! isset($grouped[$g])) {
$grouped[$g] = ['gestione' => $g, 'fatture_count' => 0, 'totale_fatture' => 0.0, 'fe_count' => 0, 'manual_count' => 0];
}
$grouped[$g]['fatture_count']++;
$grouped[$g]['totale_fatture'] += (float) ($item['importo'] ?? 0);
$grouped[$g]['fe_count']++;
}
$out = array_values($grouped);
usort($out, fn($a, $b) => strcmp((string) $a['gestione'], (string) $b['gestione']));
foreach ($out as &$row) {
$row['totale_fatture'] = round((float) $row['totale_fatture'], 2);
}
return $out;
}
/** @return array<int, array<string,mixed>> */
public function getAcquaLettureCondominiProperty(): array
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return [];
}
return StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua'))
->with([
'unitaImmobiliare:id,codice_unita,interno,scala',
'servizio:id,nome,contatore_matricola',
])
->tap(fn(Builder $query) => $this->applyActiveYearFilterToReadingQuery($query))
->orderByDesc('created_at')
->limit(60)
->get()
->map(function (StabileServizioLettura $row): array {
return [
'id' => (int) $row->id,
'data_ricezione' => optional($row->created_at)?->format('d/m/Y H:i'),
'servizio' => (string) ($row->servizio?->nome ?? 'Acqua'),
'matricola' => (string) ($row->servizio?->contatore_matricola ?? '—'),
'unita' => trim((string) ($row->unitaImmobiliare?->codice_unita ?? '') . ' ' . (string) ($row->unitaImmobiliare?->interno ?? '')),
'canale' => (string) ($row->canale_acquisizione ?? '—'),
'riferimento' => (string) ($row->riferimento_acquisizione ?? '—'),
'periodo' => ((string) optional($row->periodo_dal)->format('d/m/Y')) . ' - ' . ((string) optional($row->periodo_al)->format('d/m/Y')),
'consumo_valore' => is_numeric($row->consumo_valore) ? (float) $row->consumo_valore : null,
'consumo_unita' => (string) ($row->consumo_unita ?: 'mc'),
];
})
->all();
}
/** @return array{totale:int,completate:int,pendenti:int,sollecitate:int} */
public function getAcquaCampagnaStatsProperty(): array
{
$rows = $this->acquaCampagnaRows;
return [
'totale' => count($rows),
'completate' => count(array_filter($rows, fn(array $row): bool => $row['status'] === 'fatta')),
'pendenti' => count(array_filter($rows, fn(array $row): bool => in_array($row['status'], ['richiesta', 'non_richiesta'], true))),
'sollecitate' => count(array_filter($rows, fn(array $row): bool => $row['status'] === 'sollecito')),
];
}
/** @return array<int,array<string,mixed>> */
public function getAcquaCampagnaRowsProperty(): array
{
$stabileId = $this->resolveActiveStabileId();
$servizio = $this->resolveActiveAcquaServizio();
if (! $stabileId || ! $servizio) {
return [];
}
$year = $this->resolveActiveAnnoGestione();
$units = UnitaImmobiliare::query()
->where('stabile_id', $stabileId)
->whereNull('deleted_at')
->orderBy('scala')
->orderBy('interno')
->orderBy('id')
->get(['id', 'codice_unita', 'denominazione', 'scala', 'interno']);
$nominativi = DB::table('unita_immobiliare_nominativi')
->whereIn('unita_immobiliare_id', $units->pluck('id')->all())
->orderByDesc('updated_at')
->orderByDesc('id')
->get(['unita_immobiliare_id', 'nominativo']);
$nominativiByUnit = [];
foreach ($nominativi as $row) {
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
if ($unitId > 0 && ! isset($nominativiByUnit[$unitId])) {
$nominativiByUnit[$unitId] = trim((string) ($row->nominativo ?? ''));
}
}
$readingRows = StabileServizioLettura::query()
->where('stabile_id', $stabileId)
->where('stabile_servizio_id', (int) $servizio->id)
->whereIn('unita_immobiliare_id', $units->pluck('id')->all())
->orderByDesc('created_at')
->orderByDesc('id')
->get();
$currentYearByUnit = [];
$latestRealByUnit = [];
foreach ($readingRows as $row) {
$unitId = (int) ($row->unita_immobiliare_id ?? 0);
if ($unitId <= 0) {
continue;
}
$rowYear = optional($row->created_at)->year ?: optional($row->periodo_al)->year;
if ((int) $rowYear === $year && ! isset($currentYearByUnit[$unitId])) {
$currentYearByUnit[$unitId] = $row;
}
if ($row->lettura_fine !== null && ! isset($latestRealByUnit[$unitId])) {
$latestRealByUnit[$unitId] = $row;
}
}
return $units->map(function (UnitaImmobiliare $unit) use ($currentYearByUnit, $latestRealByUnit, $nominativiByUnit, $servizio): array {
$current = $currentYearByUnit[(int) $unit->id] ?? null;
$latestReal = $latestRealByUnit[(int) $unit->id] ?? null;
$status = 'non_richiesta';
if ($current && $current->lettura_fine !== null) {
$status = 'fatta';
} elseif ($current && $current->sollecito_inviato_at) {
$status = 'sollecito';
} elseif ($current && $current->richiesta_lettura_inviata_at) {
$status = 'richiesta';
}
return [
'unit_id' => (int) $unit->id,
'unit_label' => trim((string) ($unit->codice_unita ?? '') . ' ' . (string) ($unit->interno ?? '')) ?: ('UI #' . (int) $unit->id),
'nominativo' => $nominativiByUnit[(int) $unit->id] ?? (trim((string) ($unit->denominazione ?? '')) ?: 'Nominativo da completare'),
'previous_reading' => $latestReal?->lettura_fine,
'request_id' => $current?->id,
'status' => $status,
'requested_at' => optional($current?->richiesta_lettura_inviata_at)->format('d/m/Y H:i'),
'deadline_at' => optional($current?->deadline_lettura_at)->format('d/m/Y'),
'completed_at' => optional($current?->created_at)->format('d/m/Y H:i'),
'latest_value' => $current?->lettura_fine,
'public_url' => $this->buildPublicAcquaReadingUrl((int) $servizio->id, (int) $unit->id, $current?->id ? (int) $current->id : null),
];
})->all();
}
/** @return array<int, array<string,mixed>> */
public function getAcquaTariffeRowsProperty(): array
{
$stabileId = $this->resolveActiveStabileId();
if (! $stabileId) {
return [];
}
return StabileServizioTariffa::query()
->where('stabile_id', $stabileId)
->whereYear('data_fattura', $this->resolveActiveAnnoGestione())
->orderByDesc('data_fattura')
->orderByDesc('id')
->limit(80)
->get()
->map(function (StabileServizioTariffa $row): array {
$scaglioni = is_array($row->tariffe_scaglioni) ? $row->tariffe_scaglioni : [];
$componenti = is_array($row->tariffe_componenti) ? $row->tariffe_componenti : [];
$ui = is_array($componenti['componenti_ui'] ?? null) ? $componenti['componenti_ui'] : [];
return [
'id' => (int) $row->id,
'fattura_elettronica_id' => (int) $row->fattura_elettronica_id,
'data_fattura' => optional($row->data_fattura)?->format('d/m/Y'),
'decorrenza_tariffe' => optional($row->decorrenza_tariffe)?->format('d/m/Y'),
'profilo' => (string) ($row->profilo_tariffario ?? '—'),
'delibera_ato' => (string) ($row->delibera_ato ?? '—'),
'delibera_arera' => (string) ($row->delibera_arera ?? '—'),
'iva_codice' => (string) ($row->codice_iva ?? '—'),
'iva_aliquota' => is_numeric($row->aliquota_iva_percentuale) ? (float) $row->aliquota_iva_percentuale : null,
'scaglioni_count' => count($scaglioni),
'ui1' => $ui['ui1_euro_mc'] ?? null,
'ui2' => $ui['ui2_euro_mc'] ?? null,
'ui3' => $ui['ui3_euro_mc'] ?? null,
'ui4' => $ui['ui4_euro_mc'] ?? null,
];
})
->all();
}
/** @return array{contratti_acea: bool, tariffe_acea_standard: bool, note: string} */
public function getAcquaLegacyArchiveAvailabilityProperty(): array
{
$hasContratti = Schema::connection('gescon_import')->hasTable('contratti_ACEA')
|| Schema::connection('gescon_import')->hasTable('contratti_acea');
$hasTariffe = Schema::connection('gescon_import')->hasTable('Tariffe_ACEA_Standard')
|| Schema::connection('gescon_import')->hasTable('tariffe_acea_standard');
return [
'contratti_acea' => $hasContratti,
'tariffe_acea_standard' => $hasTariffe,
'note' => ($hasContratti || $hasTariffe)
? 'Archivio legacy disponibile su connessione gescon_import.'
: 'Tabelle legacy non presenti ora su gescon_import: caricare import da parti_comuni.mdb per abilitarle.',
];
}
/** @return array<int, array{cod_spe: string, totale: float}> */
public function getAcquaAltreVociLegacyProperty(): array
{
if (! Schema::connection('gescon_import')->hasTable('operazioni')) {
return [];
}
$legacyCodes = $this->resolveLegacyCodiciStabile();
$year = $this->resolveActiveAnnoGestione();
$query = DB::connection('gescon_import')
->table('operazioni')
->where('gestione', 'O')
->where('cod_spe', 'like', 'AC%')
->where('cod_spe', '!=', 'AC1')
->where('cod_spe', '!=', 'AC2');
if ($legacyCodes !== [] && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) {
$query->whereIn('cod_stabile', $legacyCodes);
}
if (Schema::connection('gescon_import')->hasColumn('operazioni', 'dt_spe')) {
$query->whereYear('dt_spe', $year);
}
$rows = $query
->select('cod_spe')
->selectRaw('SUM(COALESCE(importo_euro, importo, 0)) as totale')
->groupBy('cod_spe')
->orderBy('cod_spe')
->get();
return $rows->map(fn($r): array=> [
'cod_spe' => (string) ($r->cod_spe ?? '—'),
'totale' => round((float) ($r->totale ?? 0), 2),
])->all();
}
}