*/ public array $acquaSelectedFeIds = []; public ?int $acquaUiEditingId = null; /** @var array */ public array $acquaUiReadingForm = []; public string $acquaCampagnaSort = 'scala_interno'; /** @var array> */ public array $acquaCampagnaEditRows = []; 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->acquaTab = $this->normalizeAcquaTab((string) request()->query('tab', 'dashboard')); $this->resetAcquaUiReadingForm(); $this->mountInteractsWithTable(); } public function setAcquaTab(string $tab): void { $this->acquaTab = $this->normalizeAcquaTab($tab); } public function toggleAcquaFeSelection(int $fatturaId): void { $selected = $this->normalizeAcquaSelectedFeIds(); if (in_array($fatturaId, $selected, true)) { $selected = array_values(array_filter($selected, static fn(int $value): bool => $value !== $fatturaId)); } else { $selected[] = $fatturaId; } $this->acquaSelectedFeIds = array_values(array_unique($selected)); } public function selectAllAcquaFe(): void { $this->acquaSelectedFeIds = $this->resolveAcquaCandidateInvoices() ->pluck('id') ->map(fn($value): int => (int) $value) ->filter(fn(int $value): bool => $value > 0) ->values() ->all(); } public function clearAcquaFeSelection(): void { $this->acquaSelectedFeIds = []; } public function startCreateAcquaUiReading(): void { $this->resetAcquaUiReadingForm(); } public function startEditAcquaUiReading(int $readingId): void { $stabileId = $this->resolveActiveStabileId(); if (! $stabileId) { return; } $record = StabileServizioLettura::query() ->where('id', $readingId) ->where('stabile_id', $stabileId) ->whereNotNull('unita_immobiliare_id') ->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua')) ->first(); if (! $record instanceof StabileServizioLettura) { Notification::make()->title('Lettura UI non trovata')->warning()->send(); return; } $this->acquaUiEditingId = (int) $record->id; $this->acquaUiReadingForm = [ 'stabile_servizio_id' => (int) $record->stabile_servizio_id, 'unita_immobiliare_id' => (int) ($record->unita_immobiliare_id ?? 0) ?: null, 'periodo_dal' => optional($record->periodo_dal)->toDateString(), 'periodo_al' => optional($record->periodo_al)->toDateString(), 'canale_acquisizione' => (string) ($record->canale_acquisizione ?? 'manuale'), 'riferimento_acquisizione' => (string) ($record->riferimento_acquisizione ?? ''), 'workflow_stato' => (string) ($record->workflow_stato ?? 'ricevuta'), 'rilevatore_tipo' => (string) ($record->rilevatore_tipo ?? 'amministrazione'), 'rilevatore_nome' => (string) ($record->rilevatore_nome ?? ''), 'lettura_precedente_valore' => $record->lettura_precedente_valore, 'lettura_inizio' => $record->lettura_inizio, 'lettura_fine' => $record->lettura_fine, 'consumo_valore' => $record->consumo_valore, 'consumo_unita' => (string) ($record->consumo_unita ?: 'mc'), 'deadline_lettura_at' => optional($record->deadline_lettura_at)->toDateString(), 'note' => trim((string) data_get($record->raw, 'inline_ui_editor.note', '')), ]; } public function cancelAcquaUiEditing(): void { $this->resetAcquaUiReadingForm(); } public function saveAcquaUiReading(): void { $stabileId = $this->resolveActiveStabileId(); $user = Auth::user(); if (! $stabileId || ! $user instanceof User) { return; } $validated = $this->validate([ 'acquaUiReadingForm.stabile_servizio_id' => ['required', 'integer'], 'acquaUiReadingForm.unita_immobiliare_id' => ['required', 'integer'], 'acquaUiReadingForm.periodo_dal' => ['nullable', 'date'], 'acquaUiReadingForm.periodo_al' => ['nullable', 'date', 'after_or_equal:acquaUiReadingForm.periodo_dal'], 'acquaUiReadingForm.canale_acquisizione' => ['nullable', 'string', 'max:50'], 'acquaUiReadingForm.riferimento_acquisizione' => ['nullable', 'string', 'max:191'], 'acquaUiReadingForm.workflow_stato' => ['nullable', 'string', 'max:50'], 'acquaUiReadingForm.rilevatore_tipo' => ['nullable', 'string', 'max:80'], 'acquaUiReadingForm.rilevatore_nome' => ['nullable', 'string', 'max:191'], 'acquaUiReadingForm.lettura_precedente_valore' => ['nullable', 'numeric', 'min:0'], 'acquaUiReadingForm.lettura_inizio' => ['nullable', 'numeric', 'min:0'], 'acquaUiReadingForm.lettura_fine' => ['nullable', 'numeric', 'min:0'], 'acquaUiReadingForm.consumo_valore' => ['nullable', 'numeric'], 'acquaUiReadingForm.consumo_unita' => ['nullable', 'string', 'max:16'], 'acquaUiReadingForm.deadline_lettura_at' => ['nullable', 'date'], 'acquaUiReadingForm.note' => ['nullable', 'string', 'max:2000'], ]); $data = $validated['acquaUiReadingForm'] ?? []; $servizio = StabileServizio::query() ->where('id', (int) ($data['stabile_servizio_id'] ?? 0)) ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->first(); $unita = UnitaImmobiliare::query() ->where('id', (int) ($data['unita_immobiliare_id'] ?? 0)) ->where('stabile_id', $stabileId) ->first(); if (! $servizio instanceof StabileServizio || ! $unita instanceof UnitaImmobiliare) { Notification::make()->title('Servizio o unità non validi')->warning()->send(); return; } $record = $this->acquaUiEditingId ? StabileServizioLettura::query() ->where('id', (int) $this->acquaUiEditingId) ->where('stabile_id', $stabileId) ->whereNotNull('unita_immobiliare_id') ->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua')) ->first() : null; if ($this->acquaUiEditingId && ! $record instanceof StabileServizioLettura) { Notification::make()->title('Lettura UI non trovata')->warning()->send(); return; } $previous = StabileServizioLettura::query() ->where('stabile_servizio_id', (int) $servizio->id) ->where('unita_immobiliare_id', (int) $unita->id) ->when($record instanceof StabileServizioLettura, fn(Builder $q) => $q->where('id', '!=', (int) $record->id)) ->whereNotNull('lettura_fine') ->orderByDesc('periodo_al') ->orderByDesc('id') ->first(); $letturaPrecedente = is_numeric($data['lettura_precedente_valore'] ?? null) ? round((float) $data['lettura_precedente_valore'], 3) : ($previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null); $letturaInizio = is_numeric($data['lettura_inizio'] ?? null) ? round((float) $data['lettura_inizio'], 3) : $letturaPrecedente; $letturaFine = is_numeric($data['lettura_fine'] ?? null) ? round((float) $data['lettura_fine'], 3) : null; $consumo = is_numeric($data['consumo_valore'] ?? null) ? round((float) $data['consumo_valore'], 3) : (($letturaFine !== null && $letturaInizio !== null) ? round($letturaFine - $letturaInizio, 3) : null); if ($consumo !== null && $consumo < 0) { $this->addError('acquaUiReadingForm.lettura_fine', 'La lettura finale non può essere inferiore alla precedente.'); return; } $raw = is_array($record?->raw ?? null) ? $record->raw : []; $raw['inline_ui_editor'] = [ 'updated_by' => (int) $user->id, 'updated_at' => now()->toIso8601String(), 'note' => trim((string) ($data['note'] ?? '')), ]; $payload = [ 'stabile_id' => $stabileId, 'stabile_servizio_id' => (int) $servizio->id, 'unita_immobiliare_id' => (int) $unita->id, 'fornitore_id' => $servizio->fornitore_id, 'periodo_dal' => $data['periodo_dal'] ?? null, 'periodo_al' => $data['periodo_al'] ?? null, 'tipologia_lettura' => 'manuale_ui', 'canale_acquisizione' => trim((string) ($data['canale_acquisizione'] ?? 'manuale')) ?: 'manuale', 'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null, 'workflow_stato' => trim((string) ($data['workflow_stato'] ?? 'ricevuta')) ?: 'ricevuta', 'deadline_lettura_at' => $data['deadline_lettura_at'] ?? null, 'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? 'amministrazione')) ?: 'amministrazione', 'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null, 'lettura_precedente_valore' => $letturaPrecedente, 'lettura_inizio' => $letturaInizio, 'lettura_fine' => $letturaFine, 'consumo_valore' => $consumo, 'consumo_unita' => trim((string) ($data['consumo_unita'] ?? 'mc')) ?: 'mc', 'raw' => $raw, ]; if ($record instanceof StabileServizioLettura) { $record->fill($payload); $record->save(); } else { $payload['created_by'] = (int) $user->id; StabileServizioLettura::query()->create($payload); } Notification::make() ->title($record instanceof StabileServizioLettura ? 'Lettura UI aggiornata' : 'Lettura UI creata') ->success() ->send(); $this->resetAcquaUiReadingForm(); } public function deleteAcquaUiReading(int $readingId): void { $stabileId = $this->resolveActiveStabileId(); if (! $stabileId) { return; } $record = StabileServizioLettura::query() ->where('id', $readingId) ->where('stabile_id', $stabileId) ->whereNotNull('unita_immobiliare_id') ->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua')) ->first(); if (! $record instanceof StabileServizioLettura) { Notification::make()->title('Lettura UI non trovata')->warning()->send(); return; } $record->delete(); if ((int) $this->acquaUiEditingId === (int) $readingId) { $this->resetAcquaUiReadingForm(); } Notification::make()->title('Lettura UI eliminata')->success()->send(); } private function normalizeAcquaTab(string $tab): string { $allowed = ['dashboard', 'fatture', 'letture', 'generale', 'tariffe', 'servizi']; return in_array($tab, $allowed, true) ? $tab : 'dashboard'; } private function resetAcquaUiReadingForm(): void { $defaultServiceId = collect($this->acquaUiServiceOptions)->keys()->map(fn($value) => (int) $value)->first(); $this->acquaUiEditingId = null; $this->acquaUiReadingForm = [ 'stabile_servizio_id' => $defaultServiceId, 'unita_immobiliare_id' => null, 'periodo_dal' => null, 'periodo_al' => null, 'canale_acquisizione' => 'manuale', 'riferimento_acquisizione' => null, 'workflow_stato' => 'ricevuta', 'rilevatore_tipo' => 'amministrazione', 'rilevatore_nome' => null, 'lettura_precedente_valore' => null, 'lettura_inizio' => null, 'lettura_fine' => null, 'consumo_valore' => null, 'consumo_unita' => 'mc', 'deadline_lettura_at' => null, 'note' => null, ]; } public function backfillAcquaFromFatturePregresse(): void { $user = Auth::user(); if (! $user instanceof User) { Notification::make()->title('Utente non autenticato.')->danger()->send(); return; } $stabileId = $this->resolveActiveStabileId(); $servizio = $this->resolveActiveAcquaServizio(); if (! $stabileId || ! $servizio instanceof StabileServizio) { Notification::make()->title('Configura prima un servizio acqua attivo per lo stabile selezionato.')->warning()->send(); return; } $year = $this->resolveActiveAnnoGestione(); $tipoGestione = in_array($this->acquaBackfillGestione, ['ordinaria', 'riscaldamento', 'straordinaria', 'tutte'], true) ? $this->acquaBackfillGestione : 'ordinaria'; $fatture = $this->resolveAcquaCandidateInvoices($tipoGestione) ->when($this->normalizeAcquaSelectedFeIds() !== [], fn($collection) => $collection->whereIn('id', $this->normalizeAcquaSelectedFeIds())); if ($fatture->isEmpty()) { Notification::make()->title('Nessuna FE acqua trovata per lo stabile/gestione selezionati.')->warning()->send(); return; } if ($this->acquaBackfillOnlyMissing) { $fatture = $fatture->filter(function (FatturaElettronica $fattura): bool { $storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : ''; if ($storedRaw === '') { return true; } $decoded = json_decode($storedRaw, true); return ! $this->payloadLooksLikeAcqua(is_array($decoded) ? $decoded : null); })->values(); } if ($fatture->isEmpty()) { Notification::make()->title('Nessuna FE acqua da rielaborare con i filtri correnti.')->warning()->send(); return; } $processed = 0; $ok = 0; $skipped = 0; $tickets = 0; $tariffe = 0; foreach ($fatture as $fattura) { $processed++; $parsed = $this->resolveAcquaParsedPayload($fattura, (int) $user->id); if (! is_array($parsed)) { $skipped++; continue; } $consumi = array_values(is_array($parsed['consumi'] ?? null) ? $parsed['consumi'] : []); $fattura->consumo_unita = 'mc'; $fattura->consumo_valore = $this->sumAcquaConsumi($consumi); $fattura->consumo_riferimento = $this->buildAcquaConsumoReference($consumi); $fattura->consumo_raw = json_encode([ 'type' => 'acqua', 'source' => 'pdf_ocr', 'codici' => is_array($parsed['codici'] ?? null) ? $parsed['codici'] : [], 'contatore' => is_array($parsed['contatore'] ?? null) ? $parsed['contatore'] : [], 'consumi' => $consumi, 'generale' => is_array($parsed['generale'] ?? null) ? $parsed['generale'] : [], 'pagamento' => is_array($parsed['pagamento'] ?? null) ? $parsed['pagamento'] : [], 'finestra_autolettura' => is_array($parsed['finestra_autolettura'] ?? null) ? $parsed['finestra_autolettura'] : [], 'riepilogo_letture' => array_values(is_array($parsed['riepilogo_letture'] ?? null) ? $parsed['riepilogo_letture'] : []), 'quadro_dettaglio' => is_array($parsed['quadro_dettaglio'] ?? null) ? $parsed['quadro_dettaglio'] : [], 'tariffe' => is_array($parsed['tariffe'] ?? null) ? $parsed['tariffe'] : [], 'iva' => is_array($parsed['iva'] ?? null) ? $parsed['iva'] : [], 'parsed_at' => now()->toISOString(), ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $fattura->save(); try { app(ConsumiAcquaTariffeIngestionService::class)->ingest($fattura, $parsed, (int) $servizio->id); $tariffe++; } catch (\Throwable) { // best-effort } $ingestion = app(ConsumiAcquaIngestionService::class)->ingest( $fattura, $parsed, (int) ($servizio->voce_spesa_id ?? 0) ?: null, (int) $user->id, true, (int) $servizio->id, ); if (($ingestion['status'] ?? null) === 'ok') { $ok++; } else { $skipped++; } $tickets += (int) ($ingestion['tickets'] ?? 0); } Notification::make() ->title('Backfill FE acqua completato') ->body("Processate: {$processed} · Agganciate: {$ok} · Saltate: {$skipped} · Ticket: {$tickets} · Tariffe aggiornate: {$tariffe}") ->success() ->send(); } 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; } $email = DB::table('unita_recapiti_servizio') ->where('unita_id', (int) ($reading->unita_immobiliare_id ?? 0)) ->where('attivo', true) ->whereNotNull('email') ->where('email', '!=', '') ->orderByRaw("CASE WHEN tipo_servizio = 'acqua' THEN 0 ELSE 1 END") ->orderByDesc('is_default') ->orderBy('ordine') ->value('email'); if (is_string($email) && trim($email) !== '') { $email = trim($email); $publicUrl = $this->buildPublicAcquaReadingUrl((int) $reading->stabile_servizio_id, (int) $reading->unita_immobiliare_id, (int) $reading->id); $body = implode("\n", array_filter([ 'Oggetto: sollecito lettura acqua', '', 'Gentile condomino,', 'ti ricordiamo di comunicare la lettura del contatore acqua tramite il link dedicato qui sotto.', $publicUrl, '', 'Se hai già inviato la lettura puoi ignorare questo messaggio.', 'In caso di problemi contatta l amministrazione.', ])); try { Mail::raw($body, function ($message) use ($email): void { $message->to($email)->subject('Sollecito lettura acqua'); }); } catch (\Throwable) { Notification::make()->title('Invio email non riuscito')->warning()->body('Il recapito esiste ma l invio del sollecito non è riuscito.')->send(); return; } } else { Notification::make()->title('Nessuna email disponibile per questa unità')->warning()->body('Compila un recapito in unita_recapiti_servizio per inviare il sollecito diretto.')->send(); return; } $reading->fill([ 'sollecito_inviato_at' => now(), 'workflow_stato' => 'sollecito_inviato', ]); $reading->save(); Notification::make()->title('Sollecito inviato e registrato')->success()->send(); } public function setAcquaCampagnaSort(string $sort): void { $this->acquaCampagnaSort = in_array($sort, ['scala_interno', 'nominativo'], true) ? $sort : 'scala_interno'; } public function saveAcquaCampagnaRow(int $unitId): void { $this->persistAcquaCampagnaRows([$unitId]); } public function saveAllAcquaCampagnaRows(): void { $this->persistAcquaCampagnaRows(array_map('intval', array_keys($this->acquaCampagnaEditRows))); } 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), ]); }), Action::make('propaga_impostazioni_acqua') ->label('Propaga impostazioni acqua') ->icon('heroicon-o-arrow-path') ->color('info') ->requiresConfirmation() ->modalHeading('Propaga template acqua agli altri stabili') ->modalDescription('Copia il template operativo acqua dello stabile attivo sugli altri stabili accessibili senza sovrascrivere utenza, cliente, contratto e matricola già presenti.') ->action(function (): void { $stats = $this->propagateAcquaSettingsToOtherStabili(); Notification::make() ->title('Propagazione acqua completata') ->body('Stabili toccati: ' . $stats['touched'] . ' · creati: ' . $stats['created'] . ' · aggiornati: ' . $stats['updated'] . ' · saltati: ' . $stats['skipped']) ->success() ->send(); }), ]) ->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(); } /** @return array */ private function normalizeAcquaSelectedFeIds(): array { return array_values(array_unique(array_filter(array_map('intval', $this->acquaSelectedFeIds), static fn(int $value): bool => $value > 0))); } private function resolveAcquaCandidateInvoices(?string $tipoGestione = null) { $stabileId = $this->resolveActiveStabileId(); if (! $stabileId) { return collect(); } $year = $this->resolveActiveAnnoGestione(); $fornitoreIds = StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->whereNotNull('fornitore_id') ->pluck('fornitore_id') ->map(fn($value): int => (int) $value) ->filter(fn(int $value): bool => $value > 0) ->values() ->all(); if ($fornitoreIds === []) { $preferredSupplier = $this->resolvePreferredAcquaSupplier($stabileId); if ($preferredSupplier instanceof Fornitore) { $fornitoreIds = [(int) $preferredSupplier->id]; } } $fornitoreIds = $this->resolveEquivalentFornitoreIds($fornitoreIds); $query = FatturaElettronica::query() ->where('stabile_id', $stabileId) ->whereYear('data_fattura', $year) ->orderByDesc('data_fattura') ->orderByDesc('id'); if ($fornitoreIds !== []) { $query->whereIn('fornitore_id', $fornitoreIds); } if ($tipoGestione !== null && $tipoGestione !== '' && $tipoGestione !== 'tutte' && Schema::hasTable('contabilita_fatture_fornitori')) { $invoiceIds = DB::table('contabilita_fatture_fornitori as f') ->leftJoin('gestioni_contabili as g', 'g.id', '=', 'f.gestione_id') ->where('f.stabile_id', $stabileId) ->whereNotNull('f.fattura_elettronica_id') ->when($fornitoreIds !== [], fn($builder) => $builder->whereIn('f.fornitore_id', $fornitoreIds)) ->where('g.tipo_gestione', $tipoGestione) ->when(Schema::hasColumn('gestioni_contabili', 'anno_gestione'), fn($builder) => $builder->where('g.anno_gestione', $year)) ->when(! Schema::hasColumn('gestioni_contabili', 'anno_gestione') && Schema::hasColumn('contabilita_fatture_fornitori', 'data_documento'), fn($builder) => $builder->whereYear('f.data_documento', $year)) ->pluck('f.fattura_elettronica_id') ->map(fn($value): int => (int) $value) ->filter(fn(int $value): bool => $value > 0) ->unique() ->values() ->all(); if ($invoiceIds === []) { return collect(); } $query->whereIn('id', $invoiceIds); } $userId = (int) (Auth::id() ?? 0); return $query->get()->filter(function (FatturaElettronica $fattura) use ($fornitoreIds, $userId): bool { if ($fornitoreIds !== [] && in_array((int) ($fattura->fornitore_id ?? 0), $fornitoreIds, true)) { return true; } return $this->payloadLooksLikeAcqua($this->resolveAcquaParsedPayload($fattura, $userId)); })->values(); } private function payloadLooksLikeAcqua(?array $payload): bool { if (! is_array($payload) || $payload === []) { return false; } if (($payload['type'] ?? null) === 'acqua') { return true; } if (! empty($payload['consumi']) || ! empty($payload['riepilogo_letture']) || ! empty($payload['finestra_autolettura'])) { return true; } $quadro = is_array($payload['quadro_dettaglio'] ?? null) ? $payload['quadro_dettaglio'] : []; foreach (['quota_fissa', 'acquedotto', 'fognatura', 'depurazione', 'oneri_perequazione', 'restituzione_acconti'] as $key) { if (isset($quadro[$key]) && is_numeric($quadro[$key])) { return true; } } return ! empty($payload['tariffe']); } private function resolveAcquaParsedPayload(FatturaElettronica $fattura, int $userId): ?array { $storedRaw = is_string($fattura->consumo_raw ?? null) ? trim((string) $fattura->consumo_raw) : ''; if ($storedRaw !== '') { $decoded = json_decode($storedRaw, true); if (is_array($decoded) && $this->payloadLooksLikeAcqua($decoded)) { return $decoded; } $parsedStoredText = app(AcquaPdfTextParser::class)->parse($storedRaw); if ($this->payloadLooksLikeAcqua($parsedStoredText)) { return $parsedStoredText; } } try { if ($userId > 0) { app(FatturaElettronicaProtocolloService::class)->ensureDocumentoProtocollato($fattura, $userId); $fattura->refresh(); } } catch (\Throwable) { // ignore } $documento = Documento::query() ->where('documentable_type', FatturaElettronica::class) ->where('documentable_id', (int) $fattura->id) ->first(); if (! $documento instanceof Documento) { return null; } $text = is_string($documento->contenuto_ocr ?? null) ? trim((string) $documento->contenuto_ocr) : ''; if ($text === '') { try { $result = app(PdfTextExtractionService::class)->extractAndStore($documento, false); if (($result['status'] ?? null) === 'ok') { $documento->refresh(); $text = is_string($documento->contenuto_ocr ?? null) ? trim((string) $documento->contenuto_ocr) : ''; } } catch (\Throwable) { $text = ''; } } if ($text === '') { return null; } $parsed = app(AcquaPdfTextParser::class)->parse($text); return $this->payloadLooksLikeAcqua($parsed) ? $parsed : null; } /** @param array> $consumi */ private function sumAcquaConsumi(array $consumi): ?float { $sum = 0.0; $hasValues = false; foreach ($consumi as $consumo) { if (! is_array($consumo) || ! is_numeric($consumo['valore'] ?? null)) { continue; } $sum += (float) $consumo['valore']; $hasValues = true; } return $hasValues ? round($sum, 3) : null; } /** @param array> $consumi */ private function buildAcquaConsumoReference(array $consumi): ?string { $first = $consumi[0] ?? null; if (! is_array($first)) { return null; } $dal = is_string($first['dal'] ?? null) ? trim((string) $first['dal']) : ''; $al = is_string($first['al'] ?? null) ? trim((string) $first['al']) : ''; if ($dal === '' || $al === '') { return null; } return $dal . ' - ' . $al; } 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, scope_note?: string} */ public function getAcquaLegacyOperazioniSummaryProperty(): array { if (! Schema::connection('gescon_import')->hasTable('operazioni')) { return ['totale' => 0.0, 'voci' => [], 'righe' => 0, 'scope_note' => 'Archivio legacy non disponibile.']; } if (! Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile')) { $rows = $this->acquaLegacyOperazioniRows; $voci = []; $totale = 0.0; foreach ($rows as $row) { $cod = strtoupper(trim((string) ($row['cod_spe'] ?? ''))); if ($cod === '') { continue; } $importo = (float) ($row['importo'] ?? 0); $voci[$cod] = round(($voci[$cod] ?? 0.0) + $importo, 2); $totale += $importo; } return [ 'totale' => round($totale, 2), 'voci' => $voci, 'righe' => count($rows), 'scope_note' => 'Riepilogo filtrato solo sulle righe legacy collegate alle fatture locali dello stabile corrente.', ]; } $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, 'scope_note' => 'Riepilogo filtrato per stabile e anno sull archivio legacy.', ]; } /** @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(); $hasCodStabile = Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_stabile'); $query = DB::connection('gescon_import') ->table('operazioni') ->where('gestione', 'O') ->whereIn('cod_spe', ['AC1', 'AC2']); if ($legacyCodes !== [] && $hasCodStabile) { $query->whereIn('cod_stabile', $legacyCodes); } if (! $hasCodStabile && Schema::connection('gescon_import')->hasColumn('operazioni', 'cod_for')) { $legacySupplierCodes = $this->resolveLegacyOperazioniSupplierCodesForCurrentStabile(); if ($legacySupplierCodes === []) { return []; } $query->whereIn('cod_for', $legacySupplierCodes); } 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'); if ($useStg) { $fornRows = DB::table('stg_fornitori_gescon') ->whereIn('cod_forn', $codForAll) ->get(['cod_forn', 'legacy_id_fornitore', 'denominazione', 'cognome', 'nome']); } else { $stagingConn = DB::connection('gescon_import'); $tableName = $stagingConn->getSchemaBuilder()->hasTable('mdb_fornitori') ? 'mdb_fornitori' : 'fornitori'; $hasIdForn = $stagingConn->getSchemaBuilder()->hasColumn($tableName, 'id_fornitore'); $hasDenom = $stagingConn->getSchemaBuilder()->hasColumn($tableName, 'denominazione'); $hasRagione = $stagingConn->getSchemaBuilder()->hasColumn($tableName, 'ragione_sociale'); $selectFields = ['cod_forn']; if ($hasIdForn) { $selectFields[] = 'id_fornitore'; } else { $selectFields[] = 'id as id_fornitore'; } if ($hasDenom) { $selectFields[] = 'denominazione'; } elseif ($hasRagione) { $selectFields[] = 'ragione_sociale as denominazione'; } if ($stagingConn->getSchemaBuilder()->hasColumn($tableName, 'cognome')) { $selectFields[] = 'cognome'; } if ($stagingConn->getSchemaBuilder()->hasColumn($tableName, 'nome')) { $selectFields[] = 'nome'; } $fornRows = $stagingConn->table($tableName) ->whereIn('cod_forn', $codForAll) ->get($selectFields); } 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; } } } } $rows = $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, ]; }); if (! $hasCodStabile) { $rows = $rows->filter(fn(array $row): bool => (int) ($row['matched_fattura_id'] ?? 0) > 0); } return $rows->values()->all(); } /** @return array */ private function resolveLegacyOperazioniSupplierCodesForCurrentStabile(): array { if (! Schema::hasTable('fornitori')) { return []; } $stabileId = $this->resolveActiveStabileId(); if (! $stabileId) { return []; } $fornitoreIds = collect($this->resolveAcquaCandidateInvoices()) ->pluck('fornitore_id') ->map(fn($value): int => (int) $value) ->filter(fn(int $value): bool => $value > 0) ->unique() ->values() ->all(); if ($fornitoreIds === []) { $fornitoreIds = DB::table('contabilita_fatture_fornitori') ->where('stabile_id', $stabileId) ->whereNotNull('fornitore_id') ->pluck('fornitore_id') ->map(fn($value): int => (int) $value) ->filter(fn(int $value): bool => $value > 0) ->unique() ->values() ->all(); } if ($fornitoreIds === []) { return []; } $legacyIds = DB::table('fornitori') ->whereIn('id', $fornitoreIds) ->whereNotNull('old_id') ->pluck('old_id') ->map(fn($value): string => trim((string) $value)) ->filter(fn(string $value): bool => $value !== '') ->unique() ->values() ->all(); if ($legacyIds === []) { return []; } $useStg = Schema::hasTable('stg_fornitori_gescon'); if ($useStg) { $rows = DB::table('stg_fornitori_gescon')->whereIn('legacy_id_fornitore', $legacyIds)->get(['cod_forn']); } else { $stagingConn = DB::connection('gescon_import'); $tableName = $stagingConn->getSchemaBuilder()->hasTable('mdb_fornitori') ? 'mdb_fornitori' : 'fornitori'; $idCol = $stagingConn->getSchemaBuilder()->hasColumn($tableName, 'id_fornitore') ? 'id_fornitore' : 'id'; $rows = $stagingConn->table($tableName)->whereIn($idCol, $legacyIds)->get(['cod_forn']); } return $rows ->pluck('cod_forn') ->map(fn($value): string => trim((string) $value)) ->filter(fn(string $value): bool => $value !== '') ->unique() ->values() ->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); } $condominCols = Schema::connection('gescon_import')->getColumnListing('condomin'); $desired = [ 'id_cond', 'scala', 'interno', 'cognome', 'nome', 'proprietario_denominazione', 'inquilino_denominazione', 'inquil_nome', ]; $actual = array_intersect($desired, $condominCols); $condRows = $condQuery->get($actual) ->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 []; } $fatture = $this->resolveAcquaCandidateInvoices(); if ($fatture->isEmpty()) { return []; } $fattureById = $fatture->keyBy(fn(FatturaElettronica $fattura): int => (int) $fattura->id); $fatturaIds = $fattureById->keys()->map(fn($value): int => (int) $value)->all(); $rowsByFatturaId = collect($this->acquaFeEstratteRows)->keyBy(fn(array $row): int => (int) ($row['fattura_id'] ?? 0)); $enrichGroup = function (array $row, array $ids) use ($rowsByFatturaId): array { $details = collect($ids) ->map(fn(int $fatturaId): ?array=> $rowsByFatturaId->get($fatturaId)) ->filter(fn(?array $detail): bool => is_array($detail)); $row['totale_mc'] = round((float) $details->sum(fn(array $detail): float => (float) ($detail['totale_mc'] ?? 0)), 3); $row['contratti'] = $details->pluck('codice_contratto')->filter()->unique()->values()->all(); $row['utenze'] = $details->pluck('codice_utenza')->filter()->unique()->values()->all(); $row['matricole'] = $details->pluck('matricola')->filter()->unique()->values()->all(); $row['cbill_codes'] = $details->pluck('cbill')->filter()->unique()->values()->all(); return $row; }; if (Schema::hasTable('contabilita_fatture_fornitori')) { $grouped = []; $seen = []; $groupIds = []; $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.fattura_elettronica_id', $fatturaIds) ->get(['f.fattura_elettronica_id', 'g.tipo_gestione']); foreach ($contabili as $row) { $fatturaId = (int) ($row->fattura_elettronica_id ?? 0); if ($fatturaId <= 0 || ! isset($fattureById[$fatturaId])) { continue; } $gestione = strtolower(trim((string) ($row->tipo_gestione ?? ''))); if ($gestione === '') { $gestione = 'selezione fe acqua'; } $bucket = $gestione . '|' . $fatturaId; if (isset($seen[$bucket])) { continue; } if (! isset($grouped[$gestione])) { $grouped[$gestione] = [ 'gestione' => $gestione, 'fatture_count' => 0, 'totale_fatture' => 0.0, 'fe_count' => 0, 'manual_count' => 0, ]; } $grouped[$gestione]['fatture_count']++; $grouped[$gestione]['fe_count']++; $grouped[$gestione]['totale_fatture'] += (float) ($fattureById[$fatturaId]->totale ?? 0); $seen[$bucket] = true; $groupIds[$gestione][] = $fatturaId; } $matchedIds = array_values(array_unique(array_map(static fn(string $bucket): int => (int) explode('|', $bucket)[1], array_keys($seen)))); foreach ($fatture as $fattura) { if (in_array((int) $fattura->id, $matchedIds, true)) { continue; } if (! isset($grouped['selezione fe acqua'])) { $grouped['selezione fe acqua'] = [ 'gestione' => 'selezione fe acqua', 'fatture_count' => 0, 'totale_fatture' => 0.0, 'fe_count' => 0, 'manual_count' => 0, ]; } $grouped['selezione fe acqua']['fatture_count']++; $grouped['selezione fe acqua']['fe_count']++; $grouped['selezione fe acqua']['totale_fatture'] += (float) ($fattura->totale ?? 0); $groupIds['selezione fe acqua'][] = (int) $fattura->id; } if ($grouped !== []) { $out = array_values($grouped); usort($out, fn($left, $right) => strcmp((string) $left['gestione'], (string) $right['gestione'])); foreach ($out as &$row) { $row['totale_fatture'] = round((float) $row['totale_fatture'], 2); $row = $enrichGroup($row, array_values(array_unique(array_map('intval', $groupIds[(string) $row['gestione']] ?? [])))); } return $out; } } return [$enrichGroup([ 'gestione' => 'selezione fe acqua', 'fatture_count' => $fatture->count(), 'totale_fatture' => round((float) $fatture->sum(fn(FatturaElettronica $fattura): float => (float) ($fattura->totale ?? 0)), 2), 'fe_count' => $fatture->count(), 'manual_count' => 0, ], $fatturaIds)]; } /** @return array{fatture:int,periodi:int,totale_mc:float,totale_spesa:float,con_finestra:int,ultima_data:?string} */ public function getAcquaFeEstratteSummaryProperty(): array { $rows = $this->acquaFeEstratteRows; if ($rows === []) { return [ 'fatture' => 0, 'periodi' => 0, 'totale_mc' => 0.0, 'totale_spesa' => 0.0, 'con_finestra' => 0, 'ultima_data' => null, ]; } $dateValues = array_values(array_filter(array_map( static fn(array $row): ?string => is_string($row['data_fattura_iso'] ?? null) && $row['data_fattura_iso'] !== '' ? (string) $row['data_fattura_iso'] : null, $rows, ))); return [ 'fatture' => count($rows), 'periodi' => (int) array_sum(array_map(static fn(array $row): int => (int) ($row['periodi_count'] ?? 0), $rows)), 'totale_mc' => round((float) array_sum(array_map(static fn(array $row): float => (float) ($row['totale_mc'] ?? 0), $rows)), 3), 'totale_spesa' => round((float) array_sum(array_map(static fn(array $row): float => (float) ($row['totale_spesa'] ?? 0), $rows)), 2), 'con_finestra' => count(array_filter($rows, static fn(array $row): bool => (bool) ($row['ha_finestra'] ?? false))), 'ultima_data' => $dateValues !== [] ? max($dateValues) : null, ]; } /** @return array{fatture:int,totale_spesa:float,totale_mc:float} */ public function getAcquaFeSelezioneSummaryProperty(): array { $selectedIds = $this->normalizeAcquaSelectedFeIds(); $rows = collect($this->acquaFeEstratteRows) ->when($selectedIds !== [], fn($collection) => $collection->whereIn('fattura_id', $selectedIds)); return [ 'fatture' => $rows->count(), 'totale_spesa' => round((float) $rows->sum(fn(array $row): float => (float) ($row['totale_spesa'] ?? 0)), 2), 'totale_mc' => round((float) $rows->sum(fn(array $row): float => (float) ($row['totale_mc'] ?? 0)), 3), ]; } /** @return array> */ public function getAcquaFeEstratteRowsProperty(): array { $stabileId = $this->resolveActiveStabileId(); if (! $stabileId) { return []; } $servizi = StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->orderByDesc('attivo') ->orderBy('id') ->get(['id', 'nome', 'contatore_matricola', 'codice_utenza', 'codice_cliente', 'codice_contratto']); $defaultServizio = $servizi->first(); $userId = (int) (Auth::id() ?? 0); $selectedIds = $this->normalizeAcquaSelectedFeIds(); $fatture = $this->resolveAcquaCandidateInvoices(); if ($fatture->isEmpty()) { return []; } return $fatture ->map(function (FatturaElettronica $fattura) use ($selectedIds, $servizi, $defaultServizio, $stabileId, $userId): array { $payload = $this->resolveAcquaParsedPayload($fattura, $userId) ?? []; $consumi = is_array($payload['consumi'] ?? null) ? $payload['consumi'] : []; $codici = is_array($payload['codici'] ?? null) ? $payload['codici'] : []; $contatore = is_array($payload['contatore'] ?? null) ? $payload['contatore'] : []; $generale = is_array($payload['generale'] ?? null) ? $payload['generale'] : []; $pagamento = is_array($payload['pagamento'] ?? null) ? $payload['pagamento'] : []; $riepilogoLetture = is_array($payload['riepilogo_letture'] ?? null) ? $payload['riepilogo_letture'] : []; $servizio = $this->resolveAcquaServizioForIdentifiers($servizi, $codici, $contatore) ?? $defaultServizio; [$periodoDal, $periodoAl] = $this->resolveAcquaPeriodsFromPayload($consumi, is_string($fattura->consumo_riferimento ?? null) ? (string) $fattura->consumo_riferimento : null); $windowFrom = $this->extractAcquaWindowDate($payload, ['dal', 'from', 'inizio', 'start']); $windowTo = $this->extractAcquaWindowDate($payload, ['al', 'to', 'fine', 'end']); $matricola = trim((string) ($contatore['matricola'] ?? $servizio?->contatore_matricola ?? '')); $codiceUtenza = trim((string) ($codici['utenza'] ?? $servizio?->codice_utenza ?? '')); $codiceCliente = trim((string) ($codici['cliente'] ?? $servizio?->codice_cliente ?? '')); $codiceContratto = trim((string) ($codici['contratto'] ?? $servizio?->codice_contratto ?? '')); $cbill = trim((string) ($pagamento['cbill'] ?? $fattura->pagamento_cbill ?? '')); $codiceAvviso = trim((string) ($pagamento['codice_avviso'] ?? $fattura->pagamento_codice_avviso ?? '')); $sia = trim((string) ($pagamento['sia'] ?? $fattura->pagamento_codice_sia ?? '')); $totaleMc = $this->sumAcquaConsumi($consumi); if ($totaleMc === null && is_numeric($fattura->consumo_valore ?? null) && strtolower(trim((string) ($fattura->consumo_unita ?? ''))) === 'mc') { $totaleMc = round((float) $fattura->consumo_valore, 3); } $letturePreview = collect($riepilogoLetture) ->filter(fn($lettura): bool => is_array($lettura)) ->map(function (array $lettura): string { $tipologia = trim((string) ($lettura['tipologia'] ?? 'Lettura')); $consumo = is_numeric($lettura['consumo'] ?? null) ? number_format((float) $lettura['consumo'], 3, ',', '.') . ' ' . trim((string) ($lettura['unita'] ?? 'mc')) : null; $dataA = is_string($lettura['data_a'] ?? null) && trim((string) $lettura['data_a']) !== '' ? $this->formatAcquaPeriodLabel(null, (string) $lettura['data_a']) : null; return trim(implode(' · ', array_filter([$tipologia !== '' ? $tipologia : null, $consumo, $dataA]))); }) ->filter() ->take(2) ->values() ->all(); return [ 'fattura_id' => (int) $fattura->id, 'numero_fattura' => trim((string) ($fattura->numero_fattura ?? $fattura->sdi_file ?? ('FE #' . (int) $fattura->id))), 'data_fattura' => optional($fattura->data_fattura)?->format('d/m/Y'), 'data_fattura_iso' => optional($fattura->data_fattura)?->format('Y-m-d'), 'servizio' => trim((string) ($servizio?->nome ?? 'Acqua stabile')), 'servizio_id' => (int) ($servizio?->id ?? 0), 'matricola' => $matricola, 'codice_utenza' => $codiceUtenza, 'codice_cliente' => $codiceCliente, 'codice_contratto' => $codiceContratto, 'cbill' => $cbill, 'codice_avviso' => $codiceAvviso, 'sia' => $sia, 'cbill_display' => $cbill !== '' ? $cbill : ($codiceAvviso !== '' ? $codiceAvviso : ''), 'numero_componenti' => is_numeric($generale['numero_componenti_totali'] ?? null) ? (int) $generale['numero_componenti_totali'] : null, 'numero_unita_abitative' => is_numeric($generale['numero_unita_abitative'] ?? null) ? (int) $generale['numero_unita_abitative'] : null, 'periodicita_fatturazione' => trim((string) ($generale['periodicita_fatturazione'] ?? '')), 'periodo_label' => $this->formatAcquaPeriodLabel($periodoDal, $periodoAl), 'periodi_count' => count($consumi), 'letture_count' => count($riepilogoLetture), 'letture_preview' => $letturePreview, 'totale_mc' => $totaleMc ?? 0.0, 'totale_spesa' => round((float) ($fattura->totale ?? 0), 2), 'ha_finestra' => $windowFrom !== null || $windowTo !== null, 'finestra_label' => $this->formatAcquaPeriodLabel($windowFrom, $windowTo), 'selected' => in_array((int) $fattura->id, $selectedIds, true), 'identificativi_noti' => $matricola !== '' || $codiceUtenza !== '' || $codiceCliente !== '' || $codiceContratto !== '' || $cbill !== '' || $codiceAvviso !== '' || $sia !== '', 'scheda_fe_url' => FatturaElettronicaScheda::getUrl(panel: 'admin-filament', parameters: ['record' => (int) $fattura->id]), 'ticket_generale_url' => TicketAcquaGenerale::getUrl(panel: 'admin-filament', parameters: ['stabile' => $stabileId, 'servizio' => (int) ($servizio?->id ?? 0)]), 'contratti_url' => ContrattiHub::getUrl(panel: 'admin-filament'), 'letture_servizio_url' => LettureServiziArchivio::getUrl(panel: 'admin-filament', parameters: ['servizio' => (int) ($servizio?->id ?? 0)]), ]; }) ->sortByDesc('data_fattura_iso') ->take(24) ->values() ->all(); } private function decodeAcquaPayload(mixed $value): array { if (is_array($value)) { return $value; } if (is_string($value) && trim($value) !== '') { $decoded = json_decode($value, true); if (is_array($decoded)) { return $decoded; } } return []; } private function extractAcquaWindowDate(array $payload, array $keys): ?string { $window = is_array($payload['finestra_autolettura'] ?? null) ? $payload['finestra_autolettura'] : []; foreach ($keys as $key) { $value = $window[$key] ?? null; if (! is_string($value) || trim($value) === '') { continue; } try { return (string) \Carbon\Carbon::parse($value)->format('Y-m-d'); } catch (\Throwable) { continue; } } return null; } private function formatAcquaPeriodLabel(?string $from, ?string $to): string { $fromLabel = $from ? \Carbon\Carbon::parse($from)->format('d/m/Y') : null; $toLabel = $to ? \Carbon\Carbon::parse($to)->format('d/m/Y') : null; if ($fromLabel && $toLabel) { return $fromLabel . ' - ' . $toLabel; } if ($fromLabel) { return 'Dal ' . $fromLabel; } if ($toLabel) { return 'Fino al ' . $toLabel; } return '—'; } /** @param \Illuminate\Support\Collection $servizi */ private function resolveAcquaServizioForIdentifiers($servizi, array $codici, array $contatore): ?StabileServizio { if (! $servizi instanceof \Illuminate\Support\Collection || $servizi->isEmpty()) { return null; } $targetMatricola = $this->normalizeAcquaIdentifier($contatore['matricola'] ?? null); $targetUtenza = $this->normalizeAcquaIdentifier($codici['utenza'] ?? null); $targetCliente = $this->normalizeAcquaIdentifier($codici['cliente'] ?? null); $targetContratto = $this->normalizeAcquaIdentifier($codici['contratto'] ?? null); return $servizi ->map(function (StabileServizio $servizio) use ($targetMatricola, $targetUtenza, $targetCliente, $targetContratto): array { $score = 0; if ($targetMatricola !== '' && $this->normalizeAcquaIdentifier($servizio->contatore_matricola) === $targetMatricola) { $score += 5; } if ($targetUtenza !== '' && $this->normalizeAcquaIdentifier($servizio->codice_utenza) === $targetUtenza) { $score += 4; } if ($targetContratto !== '' && $this->normalizeAcquaIdentifier($servizio->codice_contratto) === $targetContratto) { $score += 3; } if ($targetCliente !== '' && $this->normalizeAcquaIdentifier($servizio->codice_cliente) === $targetCliente) { $score += 2; } return ['servizio' => $servizio, 'score' => $score]; }) ->sortByDesc('score') ->first(fn(array $candidate): bool => (int) ($candidate['score'] ?? 0) > 0)['servizio'] ?? null; } private function normalizeAcquaIdentifier(mixed $value): string { return strtoupper(preg_replace('/[^A-Z0-9]/', '', trim((string) $value)) ?: ''); } /** @param array> $consumi */ private function resolveAcquaPeriodsFromPayload(array $consumi, ?string $fallbackRange): array { $periodoDal = collect($consumi)->pluck('dal')->filter()->sort()->first(); $periodoAl = collect($consumi)->pluck('al')->filter()->sort()->last(); if (is_string($periodoDal) && $periodoDal !== '' && is_string($periodoAl) && $periodoAl !== '') { return [$periodoDal, $periodoAl]; } if (! is_string($fallbackRange) || trim($fallbackRange) === '') { return [$periodoDal, $periodoAl]; } if (preg_match('/(\d{4}-\d{2}-\d{2})\s*-\s*(\d{4}-\d{2}-\d{2})/', $fallbackRange, $matches)) { return [$matches[1], $matches[2]]; } return [$periodoDal, $periodoAl]; } 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(); } /** @return array */ public function getAcquaUiServiceOptionsProperty(): array { $stabileId = $this->resolveActiveStabileId(); if (! $stabileId) { return []; } return StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->where('attivo', true) ->orderBy('nome') ->orderBy('contatore_matricola') ->get(['id', 'nome', 'contatore_matricola', 'codice_utenza']) ->mapWithKeys(function (StabileServizio $service): array { $parts = array_filter([ trim((string) ($service->nome ?? '')) ?: ('Servizio #' . (int) $service->id), trim((string) ($service->contatore_matricola ?? '')) !== '' ? 'Matr. ' . trim((string) $service->contatore_matricola) : null, trim((string) ($service->codice_utenza ?? '')) !== '' ? 'Utenza ' . trim((string) $service->codice_utenza) : null, ]); return [(int) $service->id => implode(' · ', $parts)]; }) ->all(); } /** @return array */ public function getAcquaUiUnitOptionsProperty(): array { $stabileId = $this->resolveActiveStabileId(); if (! $stabileId) { return []; } return UnitaImmobiliare::query() ->where('stabile_id', $stabileId) ->whereNull('deleted_at') ->orderBy('scala') ->orderBy('interno') ->orderBy('id') ->get(['id', 'codice_unita', 'scala', 'interno', 'denominazione']) ->mapWithKeys(function (UnitaImmobiliare $unit): array { $label = trim(implode(' ', array_filter([ trim((string) ($unit->codice_unita ?? '')), trim((string) ($unit->scala ?? '')) !== '' ? 'Scala ' . trim((string) $unit->scala) : null, trim((string) ($unit->interno ?? '')) !== '' ? 'Int. ' . trim((string) $unit->interno) : null, trim((string) ($unit->denominazione ?? '')), ]))); return [(int) $unit->id => ($label !== '' ? $label : ('UI #' . (int) $unit->id))]; }) ->all(); } public function getAcquaRipartizionePrintUrlProperty(): string { return $this->buildAcquaRipartizioneRoute('admin.acqua-ripartizione.print'); } public function getAcquaRipartizionePdfUrlProperty(): string { return $this->buildAcquaRipartizioneRoute('admin.acqua-ripartizione.pdf'); } private function buildAcquaRipartizioneRoute(string $routeName): string { $params = [ 'stabile_id' => $this->resolveActiveStabileId(), 'year' => $this->resolveActiveAnnoGestione(), ]; $selectedIds = $this->normalizeAcquaSelectedFeIds(); if ($selectedIds !== []) { $params['ids'] = implode(',', $selectedIds); } return route($routeName, $params); } /** @return array{touched:int,created:int,updated:int,skipped:int} */ private function propagateAcquaSettingsToOtherStabili(): array { $user = Auth::user(); $currentStabileId = $this->resolveActiveStabileId(); if (! $user instanceof User || ! $currentStabileId) { return ['touched' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0]; } $template = StabileServizio::query() ->where('stabile_id', $currentStabileId) ->where('tipo', 'acqua') ->orderByDesc('attivo') ->orderBy('id') ->first(); if (! $template instanceof StabileServizio) { return ['touched' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0]; } $templateMeta = is_array($template->meta ?? null) ? $template->meta : []; $targets = StabileContext::accessibleStabili($user) ->filter(fn(Stabile $stabile): bool => (int) $stabile->id !== (int) $currentStabileId) ->values(); $stats = ['touched' => 0, 'created' => 0, 'updated' => 0, 'skipped' => 0]; foreach ($targets as $target) { $service = StabileServizio::query() ->where('stabile_id', (int) $target->id) ->where('tipo', 'acqua') ->orderByDesc('attivo') ->orderBy('id') ->first(); $targetSupplier = $this->resolvePreferredAcquaSupplier((int) $target->id); $payload = [ 'tipo' => 'acqua', 'nome' => trim((string) ($service?->nome ?: $template->nome ?: 'Utenza acqua - Acea Ato2')), 'fornitore_id' => (int) ($targetSupplier?->id ?: $service?->fornitore_id ?: $template->fornitore_id) ?: null, 'voce_spesa_id' => (int) ($service?->voce_spesa_id ?: $template->voce_spesa_id) ?: null, 'attivo' => true, 'meta' => array_merge(is_array($service?->meta ?? null) ? $service->meta : [], [ 'common_asset_label' => $templateMeta['common_asset_label'] ?? null, 'common_asset_scope' => $templateMeta['common_asset_scope'] ?? null, 'allocation_scope' => $templateMeta['allocation_scope'] ?? null, 'fiscal_scope' => $templateMeta['fiscal_scope'] ?? null, 'has_dedicated_meter' => (bool) ($templateMeta['has_dedicated_meter'] ?? false), 'counter_scope' => $templateMeta['counter_scope'] ?? 'assente', 'served_area_label' => $templateMeta['served_area_label'] ?? null, 'canale_acquisizione' => $templateMeta['canale_acquisizione'] ?? null, 'email_raccolta_letture' => $templateMeta['email_raccolta_letture'] ?? null, 'whatsapp_raccolta_letture' => $templateMeta['whatsapp_raccolta_letture'] ?? null, 'costo_lettura_unitario' => $templateMeta['costo_lettura_unitario'] ?? null, 'costo_ripartizione' => $templateMeta['costo_ripartizione'] ?? null, 'ripartizione_esterna' => (bool) ($templateMeta['ripartizione_esterna'] ?? false), 'operative_notes' => $templateMeta['operative_notes'] ?? null, 'propagated_from_stabile_id' => (int) $currentStabileId, 'propagated_at' => now()->toIso8601String(), ]), ]; if ($service instanceof StabileServizio) { $service->fill($payload); $service->save(); $stats['updated']++; } else { StabileServizio::query()->create(array_merge($payload, [ 'stabile_id' => (int) $target->id, 'codice_utenza' => null, 'codice_cliente' => null, 'codice_contratto' => null, 'contatore_matricola' => null, ])); $stats['created']++; } $stats['touched']++; } return $stats; } /** @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> */ public function getAcquaLettureGeneraliProperty(): array { $stabileId = $this->resolveActiveStabileId(); if (! $stabileId) { return []; } return StabileServizioLettura::query() ->where('stabile_id', $stabileId) ->whereNull('unita_immobiliare_id') ->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua')) ->with([ 'servizio:id,nome,codice_utenza,codice_cliente,codice_contratto,contatore_matricola', 'fatturaElettronica:id,numero_fattura,data_fattura', ]) ->tap(fn(Builder $query) => $this->applyActiveYearFilterToReadingQuery($query)) ->orderByDesc('periodo_al') ->orderByDesc('id') ->limit(60) ->get() ->map(function (StabileServizioLettura $row): array { $schedule = is_array(data_get($row->raw, 'acea_schedule')) ? data_get($row->raw, 'acea_schedule') : []; $windowStart = data_get($schedule, 'window_start'); $windowEnd = data_get($schedule, 'window_end'); $windowLabel = trim(implode(' - ', array_filter([ $windowStart ? optional(now()->parse((string) $windowStart))->format('d/m/Y') : null, $windowEnd ? optional(now()->parse((string) $windowEnd))->format('d/m/Y') : null, ]))); 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 ?? '—'), 'codice_utenza' => (string) ($row->servizio?->codice_utenza ?? '—'), 'codice_cliente' => (string) ($row->servizio?->codice_cliente ?? '—'), 'codice_contratto' => (string) ($row->servizio?->codice_contratto ?? '—'), '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')), 'lettura_fine' => is_numeric($row->lettura_fine) ? (float) $row->lettura_fine : null, 'consumo_valore' => is_numeric($row->consumo_valore) ? (float) $row->consumo_valore : null, 'consumo_unita' => (string) ($row->consumo_unita ?: 'mc'), 'protocollo' => (string) ($row->protocollo_numero ?? '—'), 'workflow' => (string) ($row->workflow_stato ?? '—'), 'reader' => (string) ($row->rilevatore_nome ?? '—'), 'window_label' => $windowLabel !== '' ? $windowLabel : null, 'visit_date' => data_get($schedule, 'visit_date'), 'fattura_label' => $row->fatturaElettronica ? trim((string) ($row->fatturaElettronica->numero_fattura ?? 'FE #' . $row->fatturaElettronica->id)) : null, 'fattura_url' => $row->fattura_elettronica_id ? FatturaElettronicaScheda::getUrl(['record' => (int) $row->fattura_elettronica_id], panel : 'admin-filament') : null, 'ticket_url' => TicketAcquaGenerale::getUrl([ 'stabile' => (int) $row->stabile_id, 'servizio' => (int) $row->stabile_servizio_id, ], panel: 'admin-filament'), 'archivio_url' => LettureServiziArchivio::getUrl([ 'servizio' => (int) $row->stabile_servizio_id, ], panel: 'admin-filament'), ]; }) ->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 ?? '')); } } $recapitiRows = DB::table('unita_recapiti_servizio') ->whereIn('unita_id', $units->pluck('id')->all()) ->where('attivo', true) ->whereNotNull('email') ->where('email', '!=', '') ->orderByRaw("CASE WHEN tipo_servizio = 'acqua' THEN 0 ELSE 1 END") ->orderByDesc('is_default') ->orderBy('ordine') ->get(['unita_id', 'email']); $emailsByUnit = []; foreach ($recapitiRows as $row) { $unitId = (int) ($row->unita_id ?? 0); if ($unitId > 0 && ! isset($emailsByUnit[$unitId])) { $emailsByUnit[$unitId] = trim((string) ($row->email ?? '')); } } $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; } } $rows = $units->map(function (UnitaImmobiliare $unit) use ($stabileId, $currentYearByUnit, $latestRealByUnit, $nominativiByUnit, $emailsByUnit, $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, 'scala' => trim((string) ($unit->scala ?? '')), 'interno' => trim((string) ($unit->interno ?? '')), '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'), 'email' => $emailsByUnit[(int) $unit->id] ?? null, '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, 'edit' => $this->resolveAcquaCampagnaEditRow((int) $unit->id, $current, $latestReal), 'public_url' => $this->buildPublicAcquaReadingUrl((int) $servizio->id, (int) $unit->id, $current?->id ? (int) $current->id : null), 'ticket_url' => TicketAcqua::getUrl([ 'stabile' => $stabileId, 'servizio' => (int) $servizio->id, 'unita' => (int) $unit->id, ], panel: 'admin-filament'), ]; })->all(); usort($rows, function (array $left, array $right): int { if ($this->acquaCampagnaSort === 'nominativo') { return strnatcasecmp((string) ($left['nominativo'] ?? ''), (string) ($right['nominativo'] ?? '')); } $leftKey = trim((string) ($left['scala'] ?? '') . ' ' . (string) ($left['interno'] ?? '')); $rightKey = trim((string) ($right['scala'] ?? '') . ' ' . (string) ($right['interno'] ?? '')); return strnatcasecmp($leftKey, $rightKey); }); return $rows; } private function resolveAcquaCampagnaEditRow(int $unitId, ?StabileServizioLettura $current, ?StabileServizioLettura $latestReal): array { if (! isset($this->acquaCampagnaEditRows[$unitId])) { $this->acquaCampagnaEditRows[$unitId] = [ 'data_lettura' => optional($current?->periodo_al)->toDateString() ?: now()->toDateString(), 'deadline_at' => optional($current?->deadline_lettura_at)->toDateString(), 'canale' => (string) ($current->canale_acquisizione ?? 'manuale'), 'riferimento' => (string) ($current->riferimento_acquisizione ?? ''), 'lettura_finale' => $current?->lettura_fine, 'note' => trim((string) data_get($current?->raw ?? [], 'campagna_editor.note', '')), 'reader_name' => trim((string) ($current->rilevatore_nome ?? Auth::user()?->name ?? '')), 'previous_reading' => $latestReal?->lettura_fine, ]; } return $this->acquaCampagnaEditRows[$unitId]; } /** @param array $unitIds */ private function persistAcquaCampagnaRows(array $unitIds): void { $stabileId = $this->resolveActiveStabileId(); $servizio = $this->resolveActiveAcquaServizio(); $user = Auth::user(); if (! $stabileId || ! $servizio || ! $user instanceof User) { return; } $saved = 0; foreach ($unitIds as $unitId) { $unitId = (int) $unitId; if ($unitId <= 0) { continue; } $unit = UnitaImmobiliare::query()->where('stabile_id', $stabileId)->find($unitId); if (! $unit instanceof UnitaImmobiliare) { continue; } $edit = is_array($this->acquaCampagnaEditRows[$unitId] ?? null) ? $this->acquaCampagnaEditRows[$unitId] : []; if ($edit === []) { continue; } $current = StabileServizioLettura::query() ->where('stabile_id', $stabileId) ->where('stabile_servizio_id', (int) $servizio->id) ->where('unita_immobiliare_id', $unitId) ->whereYear('created_at', $this->resolveActiveAnnoGestione()) ->orderByDesc('created_at') ->orderByDesc('id') ->first(); $previous = StabileServizioLettura::query() ->where('stabile_id', $stabileId) ->where('stabile_servizio_id', (int) $servizio->id) ->where('unita_immobiliare_id', $unitId) ->whereNotNull('lettura_fine') ->when($current instanceof StabileServizioLettura, fn(Builder $q) => $q->where('id', '!=', (int) $current->id)) ->orderByDesc('periodo_al') ->orderByDesc('id') ->first(); $previousValue = is_numeric($edit['previous_reading'] ?? null) ? round((float) $edit['previous_reading'], 3) : ($previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null); $finalValue = is_numeric($edit['lettura_finale'] ?? null) ? round((float) $edit['lettura_finale'], 3) : null; $consumo = ($previousValue !== null && $finalValue !== null) ? round($finalValue - $previousValue, 3) : null; if ($consumo !== null && $consumo < 0) { continue; } $payload = [ 'stabile_id' => $stabileId, 'stabile_servizio_id' => (int) $servizio->id, 'unita_immobiliare_id' => $unitId, 'fornitore_id' => $servizio->fornitore_id, 'periodo_dal' => $previous?->periodo_al, 'periodo_al' => $edit['data_lettura'] ?? null, 'tipologia_lettura' => $finalValue !== null ? 'manuale_ui' : 'richiesta_autolettura', 'canale_acquisizione' => trim((string) ($edit['canale'] ?? 'manuale')) ?: 'manuale', 'riferimento_acquisizione' => trim((string) ($edit['riferimento'] ?? '')) ?: null, 'workflow_stato' => $finalValue !== null ? 'ricevuta' : ($current?->workflow_stato ?: 'richiesta_inviata'), 'richiesta_lettura_inviata_at' => $current?->richiesta_lettura_inviata_at ?: now(), 'deadline_lettura_at' => $edit['deadline_at'] ?? null, 'rilevatore_tipo' => 'amministrazione', 'rilevatore_nome' => trim((string) ($edit['reader_name'] ?? $user->name)) ?: $user->name, 'lettura_precedente_valore' => $previousValue, 'lettura_inizio' => $previousValue, 'lettura_fine' => $finalValue, 'consumo_valore' => $consumo, 'consumo_unita' => 'mc', 'raw' => array_merge(is_array($current?->raw ?? null) ? $current->raw : [], [ 'campagna_editor' => [ 'note' => trim((string) ($edit['note'] ?? '')), 'updated_at' => now()->toIso8601String(), 'updated_by' => (int) $user->id, ], ]), ]; if ($current instanceof StabileServizioLettura) { $current->fill($payload); $current->save(); } else { $payload['created_by'] = (int) $user->id; StabileServizioLettura::query()->create($payload); } $saved++; } Notification::make()->title('Campagna letture aggiornata')->body('Righe salvate: ' . $saved)->success()->send(); } /** @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(); } }