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 = $this->resolvePreferredAcquaSupplier($stabileId); 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 */ 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 */ 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 */ 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 */ 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 */ 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 $currentMeta * @param array $data * @return array */ 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 $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 */ 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 */ 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 */ 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, 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> */ 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 = $this->resolvePreferredAcquaSupplier($stabileId); 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?->ragione_sociale, ]; } /** @return array */ 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 */ 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(); $fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreIds); if (Schema::hasTable('fornitori') && Schema::hasColumn('fornitori', 'fe_features')) { $waterEnabledSupplierIds = Fornitore::query() ->where(function (Builder $query) use ($stabileId): Builder { return $query->whereExists(function ($subQuery) use ($stabileId) { $subQuery->selectRaw('1') ->from('fatture_elettroniche as fe') ->whereColumn('fe.fornitore_id', 'fornitori.id') ->where('fe.stabile_id', $stabileId); })->orWhereExists(function ($subQuery) use ($stabileId) { $subQuery->selectRaw('1') ->from('contabilita_fatture_fornitori as cff') ->whereColumn('cff.fornitore_id', 'fornitori.id') ->where('cff.stabile_id', $stabileId); }); }) ->whereRaw("JSON_EXTRACT(fe_features, '$.acqua.enabled') = true") ->pluck('id') ->map(fn($v) => (int) $v) ->filter(fn($v) => $v > 0) ->unique() ->values() ->all(); $fornitoreIds = array_values(array_unique(array_merge( $fornitoreIds, $this->resolveEquivalentFornitoreIds($waterEnabledSupplierIds) ))); } if ($fornitoreIds === []) { $preferredSupplier = $this->resolvePreferredAcquaSupplier($stabileId); if ($preferredSupplier) { $fornitoreIds = $this->resolveEquivalentFornitoreIds([(int) $preferredSupplier->id]); } } 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; } private function resolvePreferredAcquaSupplier(?int $stabileId = null): ?Fornitore { if (! Schema::hasTable('fornitori')) { return null; } $serviceSupplierId = null; if ($stabileId) { $serviceSupplierId = StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->whereNotNull('fornitore_id') ->orderByDesc('attivo') ->orderBy('id') ->value('fornitore_id'); } $candidates = Fornitore::query() ->where(function (Builder $query): Builder { return $query->whereRaw('LOWER(ragione_sociale) like ?', ['%acea%ato2%']) ->orWhereRaw('LOWER(ragione_sociale) like ?', ['%acea acqua%']); }) ->get([ 'id', 'amministratore_id', 'rubrica_id', 'ragione_sociale', 'partita_iva', 'codice_fiscale', 'old_id', 'fe_features', ]); if ($candidates->isEmpty()) { return null; } $userAdminId = null; $user = Auth::user(); if ($user instanceof User && isset($user->amministratore_id) && is_numeric($user->amministratore_id)) { $userAdminId = (int) $user->amministratore_id; } $stableInvoiceCounts = []; if ($stabileId) { $candidateIds = $candidates->pluck('id') ->map(fn($value) => (int) $value) ->filter(fn($value) => $value > 0) ->all(); if ($candidateIds !== []) { $feCounts = DB::table('fatture_elettroniche') ->selectRaw('fornitore_id, COUNT(*) as aggregate_count') ->where('stabile_id', $stabileId) ->whereIn('fornitore_id', $candidateIds) ->groupBy('fornitore_id') ->pluck('aggregate_count', 'fornitore_id') ->all(); $contabiliCounts = DB::table('contabilita_fatture_fornitori') ->selectRaw('fornitore_id, COUNT(*) as aggregate_count') ->where('stabile_id', $stabileId) ->whereIn('fornitore_id', $candidateIds) ->groupBy('fornitore_id') ->pluck('aggregate_count', 'fornitore_id') ->all(); foreach ($candidateIds as $candidateId) { $stableInvoiceCounts[$candidateId] = (int) ($feCounts[$candidateId] ?? 0) + (int) ($contabiliCounts[$candidateId] ?? 0); } } } $sorted = $candidates->sort(function (Fornitore $left, Fornitore $right) use ($serviceSupplierId, $userAdminId, $stableInvoiceCounts): int { $leftScore = $this->scorePreferredAcquaSupplier($left, $serviceSupplierId, $userAdminId, $stableInvoiceCounts); $rightScore = $this->scorePreferredAcquaSupplier($right, $serviceSupplierId, $userAdminId, $stableInvoiceCounts); if ($leftScore === $rightScore) { return (int) $left->id <=> (int) $right->id; } return $rightScore <=> $leftScore; }); return $sorted->first(); } /** @param array $supplierIds */ private function resolveEquivalentFornitoreIds(array $supplierIds): array { $supplierIds = array_values(array_unique(array_filter(array_map('intval', $supplierIds), fn(int $value): bool => $value > 0))); if ($supplierIds === [] || ! Schema::hasTable('fornitori')) { return $supplierIds; } $suppliers = Fornitore::query() ->whereIn('id', $supplierIds) ->get(['id', 'rubrica_id', 'partita_iva', 'codice_fiscale', 'old_id']); if ($suppliers->isEmpty()) { return $supplierIds; } $rubricaIds = $suppliers->pluck('rubrica_id') ->filter(fn($value) => is_numeric($value) && (int) $value > 0) ->map(fn($value) => (int) $value) ->unique() ->values() ->all(); $partiteIva = $suppliers->pluck('partita_iva') ->map(fn($value) => trim((string) $value)) ->filter(fn(string $value): bool => $value !== '') ->unique() ->values() ->all(); $codiciFiscali = $suppliers->pluck('codice_fiscale') ->map(fn($value) => trim((string) $value)) ->filter(fn(string $value): bool => $value !== '') ->unique() ->values() ->all(); $legacyIds = $suppliers->pluck('old_id') ->filter(fn($value) => is_numeric($value) && (int) $value > 0) ->map(fn($value) => (int) $value) ->unique() ->values() ->all(); $matches = Fornitore::query() ->where(function (Builder $query) use ($supplierIds, $rubricaIds, $partiteIva, $codiciFiscali, $legacyIds): Builder { $query->whereIn('id', $supplierIds); if ($rubricaIds !== []) { $query->orWhereIn('rubrica_id', $rubricaIds); } if ($partiteIva !== []) { $query->orWhereIn('partita_iva', $partiteIva); } if ($codiciFiscali !== []) { $query->orWhereIn('codice_fiscale', $codiciFiscali); } if ($legacyIds !== []) { $query->orWhereIn('old_id', $legacyIds); } return $query; }) ->pluck('id') ->map(fn($value) => (int) $value) ->filter(fn($value) => $value > 0) ->unique() ->values() ->all(); return $matches !== [] ? $matches : $supplierIds; } /** @param array $stableInvoiceCounts */ private function scorePreferredAcquaSupplier(Fornitore $supplier, ?int $serviceSupplierId, ?int $userAdminId, array $stableInvoiceCounts): int { $score = 0; if ($serviceSupplierId && (int) $supplier->id === $serviceSupplierId) { $score += 4000; } $waterFeatures = is_array($supplier->fe_features ?? null) ? $supplier->fe_features : []; if ((bool) data_get($waterFeatures, 'acqua.enabled', false)) { $score += 1000; } if ($userAdminId && isset($supplier->amministratore_id) && (int) $supplier->amministratore_id === $userAdminId) { $score += 500; } $score += ((int) ($stableInvoiceCounts[(int) $supplier->id] ?? 0)) * 25; return $score; } /** @return array> */ 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> */ 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> */ 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 */ 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(); } }