From d95fba5740a0d63d471f0323081c91cf5a1d16b2 Mon Sep 17 00:00:00 2001 From: michele Date: Mon, 13 Apr 2026 11:54:04 +0000 Subject: [PATCH] Add general water reading workflow and fix mobile water flows --- app/Filament/Pages/Condomini/StabilePage.php | 2 +- app/Filament/Pages/Condomini/StabilePosta.php | 9 +- .../Contabilita/FatturaElettronicaScheda.php | 19 + app/Filament/Pages/Supporto/TicketAcqua.php | 92 +- .../Pages/Supporto/TicketAcquaGenerale.php | 883 ++++++++++++++++++ .../pages/condomini/stabile-posta.blade.php | 2 +- .../fattura-elettronica-scheda.blade.php | 2 +- .../supporto/ticket-acqua-generale.blade.php | 375 ++++++++ .../pages/supporto/ticket-acqua.blade.php | 1 + .../pages/supporto/ticket-mobile.blade.php | 31 +- 10 files changed, 1403 insertions(+), 13 deletions(-) create mode 100644 app/Filament/Pages/Supporto/TicketAcquaGenerale.php create mode 100644 resources/views/filament/pages/supporto/ticket-acqua-generale.blade.php diff --git a/app/Filament/Pages/Condomini/StabilePage.php b/app/Filament/Pages/Condomini/StabilePage.php index f5697eb..5788a10 100644 --- a/app/Filament/Pages/Condomini/StabilePage.php +++ b/app/Filament/Pages/Condomini/StabilePage.php @@ -1227,7 +1227,7 @@ public function getStableGoogleConnectUrl(): string return route('oauth.google.redirect', [ 'account_key' => 'stabile-' . ($this->stabile?->id ?? 0) . '-pec', 'label' => $label, - 'return_to' => request()->getRequestUri(), + 'return_to' => $this->getStabilePostaUrl(), ]); } diff --git a/app/Filament/Pages/Condomini/StabilePosta.php b/app/Filament/Pages/Condomini/StabilePosta.php index 9d3a88a..d08b8e0 100644 --- a/app/Filament/Pages/Condomini/StabilePosta.php +++ b/app/Filament/Pages/Condomini/StabilePosta.php @@ -261,7 +261,7 @@ public function getStableGoogleConnectUrl(): string return route('oauth.google.redirect', [ 'account_key' => 'stabile-' . ($this->stabile?->id ?? 0) . '-pec', 'label' => $label, - 'return_to' => request()->getRequestUri(), + 'return_to' => $this->getReturnUrl(), ]); } @@ -270,7 +270,12 @@ public function getTestGoogleConnectUrl(): string return route('oauth.google.redirect', [ 'account_key' => 'gmail-test', 'label' => 'Account Google test', - 'return_to' => request()->getRequestUri(), + 'return_to' => $this->getReturnUrl(), ]); } + + public function getReturnUrl(): string + { + return static::getUrl(['stabile_id' => (int) ($this->stabile?->id ?? 0)], panel: 'admin-filament'); + } } diff --git a/app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php b/app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php index aaa71d0..91397f0 100644 --- a/app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php +++ b/app/Filament/Pages/Contabilita/FatturaElettronicaScheda.php @@ -54,6 +54,25 @@ class FatturaElettronicaScheda extends Page /** @var array */ public array $acquaPdfPreview = []; + public function getDocumentoProtocolloConAnnoProperty(): ?string + { + if (! $this->documento instanceof Documento) { + return null; + } + + $protocollo = trim((string) ($this->documento->numero_protocollo ?? '')); + if ($protocollo === '') { + return null; + } + + $year = $this->documento->data_documento?->format('Y') + ?: $this->documento->created_at?->format('Y') + ?: $this->fattura->data_fattura?->format('Y') + ?: now()->format('Y'); + + return $year . ' · ' . $protocollo; + } + public function getFornitoreAgganciatoLabel(): ?string { $f = $this->fattura->fornitore; diff --git a/app/Filament/Pages/Supporto/TicketAcqua.php b/app/Filament/Pages/Supporto/TicketAcqua.php index ca005b8..8ab4d21 100644 --- a/app/Filament/Pages/Supporto/TicketAcqua.php +++ b/app/Filament/Pages/Supporto/TicketAcqua.php @@ -455,6 +455,8 @@ private function loadServiceOptions(): void return; } + $this->ensureWaterServiceBucket(); + $this->servizioOptions = StabileServizio::query() ->where('stabile_id', (int) $this->stabileId) ->where('tipo', 'acqua') @@ -497,8 +499,6 @@ private function loadUnitOptions(): void ->where(function ($query): void { $query->whereNull('attiva')->orWhere('attiva', true); }) - ->orderBy('codice_unita') - ->orderBy('id') ->get(['id', 'codice_unita', 'denominazione', 'scala', 'piano', 'interno']) ->map(function (UnitaImmobiliare $unita): array { $bits = [trim((string) ($unita->codice_unita ?: ('UI #' . $unita->id)))]; @@ -517,9 +517,10 @@ private function loadUnitOptions(): void } $readerLabel = $this->resolveUnitReaderLabel($unita); + $searchNames = $this->resolveUnitSearchNames($unita); $searchText = mb_strtolower(implode(' ', array_filter([ implode(' ', array_filter($bits)), - $readerLabel, + ...$searchNames, ]))); return [ @@ -528,10 +529,15 @@ private function loadUnitOptions(): void 'reader_label' => $readerLabel, 'folder_prefix' => $this->buildUnitFolderPrefix($unita), 'search_text' => $searchText, + 'sort_key' => $this->buildUnitNaturalSortKey($unita, $readerLabel), ]; }) ->all(); + usort($this->unitaOptions, static function (array $left, array $right): int { + return strnatcasecmp((string) ($left['sort_key'] ?? ''), (string) ($right['sort_key'] ?? '')); + }); + $unitIds = array_column($this->unitaOptions, 'id'); if (! in_array((int) $this->unitaId, $unitIds, true)) { $this->unitaId = $unitIds[0] ?? null; @@ -696,6 +702,38 @@ private function syncSuggestedReaderName(): void } } + private function ensureWaterServiceBucket(): void + { + if (! $this->stabileId) { + return; + } + + $hasActiveWaterService = StabileServizio::query() + ->where('stabile_id', (int) $this->stabileId) + ->where('tipo', 'acqua') + ->where('attivo', true) + ->exists(); + + if ($hasActiveWaterService) { + return; + } + + StabileServizio::query()->firstOrCreate( + [ + 'stabile_id' => (int) $this->stabileId, + 'tipo' => 'acqua', + 'nome' => 'Archivio letture acqua', + ], + [ + 'attivo' => true, + 'meta' => [ + 'auto_created_by' => 'ticket-acqua', + 'purpose' => 'reading-bucket', + ], + ] + ); + } + private function ensureDefaultSelections(): void { if (! $this->stabileId && $this->stabileOptions !== []) { @@ -752,6 +790,54 @@ private function resolveUnitReaderLabel(UnitaImmobiliare $unita): string return trim((string) ($unita->denominazione ?? '')); } + /** + * @return array + */ + private function resolveUnitSearchNames(UnitaImmobiliare $unita): array + { + $names = []; + + foreach (($unita->rubricaRuoliAttivi ?? collect()) as $role) { + $contact = $role->contatto; + if (! $contact) { + continue; + } + + $label = trim((string) ($contact->nome_completo ?: $contact->ragione_sociale)); + if ($label !== '') { + $names[] = $label; + } + } + + foreach (($unita->nominativiStorici ?? collect()) as $historic) { + $label = trim((string) ($historic->nominativo ?? '')); + if ($label !== '') { + $names[] = $label; + } + } + + $fallback = trim((string) ($unita->denominazione ?? '')); + if ($fallback !== '') { + $names[] = $fallback; + } + + return array_values(array_unique(array_filter($names))); + } + + private function buildUnitNaturalSortKey(UnitaImmobiliare $unita, string $readerLabel): string + { + $parts = array_filter([ + trim((string) ($unita->scala ?? '')), + trim((string) ($unita->interno ?? '')), + trim((string) ($unita->codice_unita ?? '')), + trim((string) ($unita->piano ?? '')), + trim($readerLabel), + (string) $unita->id, + ]); + + return implode(' ', $parts); + } + private function buildUnitFolderPrefix(UnitaImmobiliare $unita): string { $parts = array_filter([ diff --git a/app/Filament/Pages/Supporto/TicketAcquaGenerale.php b/app/Filament/Pages/Supporto/TicketAcquaGenerale.php new file mode 100644 index 0000000..6bfe24c --- /dev/null +++ b/app/Filament/Pages/Supporto/TicketAcquaGenerale.php @@ -0,0 +1,883 @@ + */ + public array $newGeneralWaterPhotos = []; + + /** @var array */ + public array $pendingGeneralWaterPhotos = []; + + /** @var array */ + public array $generalWaterPhotoDescriptions = []; + + /** @var array */ + public array $generalWaterUploadCodes = []; + + public string $generalWaterDraftProtocol = ''; + + public int $generalWaterDraftSequence = 1; + + /** @var array */ + public array $stabileOptions = []; + + /** @var array */ + public array $servizioOptions = []; + + /** @var array */ + public array $readerOptions = []; + + /** @var array|null */ + public ?array $lastGeneralReading = null; + + /** @var array> */ + public array $generalWaterClientMetadata = []; + + public static function canAccess(): bool + { + $user = Auth::user(); + + return $user instanceof User + && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); + } + + public function mount(): void + { + $user = Auth::user(); + $this->dataLettura = now()->toDateString(); + $this->rilevatoreNome = $user instanceof User ? trim((string) $user->name) : null; + $this->resetGeneralWaterDraftUploadState(); + $this->loadStabileOptions(); + $this->bootstrapDefaultSelection(); + $this->loadServiceOptions(); + $this->loadReaderOptions(); + $this->loadLastGeneralReading(); + } + + public function updatedStabileId(): void + { + $this->loadServiceOptions(); + $this->loadReaderOptions(); + $this->loadLastGeneralReading(); + } + + public function updatedServizioId(): void + { + $this->loadLastGeneralReading(); + } + + public function updatedRilevatoreUserId(): void + { + foreach ($this->readerOptions as $option) { + if ((int) ($option['id'] ?? 0) !== (int) ($this->rilevatoreUserId ?? 0)) { + continue; + } + + $this->rilevatoreNome = (string) ($option['label'] ?? $this->rilevatoreNome); + $this->rilevatoreTipo = (string) ($option['tipo'] ?? $this->rilevatoreTipo); + break; + } + } + + public function updatedPendingGeneralWaterPhotos(): void + { + $this->appendPendingGeneralWaterPhotos(); + } + + public function removeGeneralWaterPhoto(int $index): void + { + $file = $this->newGeneralWaterPhotos[$index] ?? null; + unset($this->newGeneralWaterPhotos[$index]); + $this->newGeneralWaterPhotos = array_values($this->newGeneralWaterPhotos); + + if (is_object($file)) { + unset($this->generalWaterUploadCodes[$this->resolveUploadQueueKey($file, $index)]); + } + + $this->syncGeneralWaterDescriptionSlots(); + } + + /** + * @param array> $items + */ + public function updateGeneralWaterClientMetadata(array $items): void + { + $this->generalWaterClientMetadata = array_values(array_filter(array_map(function (mixed $item): ?array { + if (! is_array($item)) { + return null; + } + + $name = trim((string) ($item['name'] ?? '')); + $size = (int) ($item['size'] ?? 0); + if ($name === '' || $size < 0) { + return null; + } + + return [ + 'source' => trim((string) ($item['source'] ?? '')), + 'name' => $name, + 'size' => $size, + 'captured_at' => trim((string) ($item['captured_at'] ?? '')), + 'device' => trim((string) ($item['device'] ?? '')), + 'gps_decimal' => is_array($item['gps_decimal'] ?? null) ? $item['gps_decimal'] : [], + 'gps_accuracy_meters' => $item['gps_accuracy_meters'] ?? null, + ]; + }, $items))); + } + + /** + * @return array}> + */ + public function getSelectedGeneralWaterUploadsProperty(): array + { + $rows = []; + + foreach ($this->newGeneralWaterPhotos as $index => $file) { + if (! is_object($file)) { + continue; + } + + $name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('foto-' . $index)); + $mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream'); + $size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0); + $protocol = $this->resolveDraftUploadCode($file, $index); + $metadata = app(TicketAttachmentUploadService::class)->mergeClientCaptureMetadata( + app(TicketAttachmentUploadService::class)->inspectUpload($file), + $this->resolveClientCaptureMetadata($name, $size), + ); + $preview = null; + + if (method_exists($file, 'temporaryUrl')) { + try { + $preview = (string) $file->temporaryUrl(); + } catch (Throwable) { + $preview = null; + } + } + + $rows[] = [ + 'index' => (int) $index, + 'name' => $this->buildDraftUploadDisplayName($protocol, $name), + 'original_name' => $name, + 'protocol' => $protocol, + 'mime' => $mime, + 'is_image' => str_starts_with($mime, 'image/'), + 'preview_url' => $preview, + 'size' => $size, + 'metadata' => is_array($metadata) ? $metadata : [], + ]; + } + + return $rows; + } + + /** + * @return array|null + */ + public function getSelectedServiceSnapshotProperty(): ?array + { + if (! $this->servizioId) { + return null; + } + + $servizio = StabileServizio::query() + ->with(['fornitore:id,ragione_sociale']) + ->where('id', (int) $this->servizioId) + ->where('stabile_id', (int) ($this->stabileId ?? 0)) + ->first(); + + if (! $servizio instanceof StabileServizio) { + return null; + } + + $smsParts = array_filter([ + trim((string) ($servizio->codice_utenza ?? '')), + trim((string) ($servizio->codice_cliente ?? '')), + trim((string) ($this->letturaAttuale ?? '')), + ]); + + return [ + 'id' => (int) $servizio->id, + 'nome' => trim((string) ($servizio->nome ?? '')), + 'fornitore' => trim((string) ($servizio->fornitore?->ragione_sociale ?? '')), + 'codice_utenza' => trim((string) ($servizio->codice_utenza ?? '')), + 'codice_cliente' => trim((string) ($servizio->codice_cliente ?? '')), + 'codice_contratto' => trim((string) ($servizio->codice_contratto ?? '')), + 'matricola' => trim((string) ($servizio->contatore_matricola ?? '')), + 'acea_sms_preview' => count($smsParts) === 3 ? implode('#', $smsParts) : '', + 'year_folder' => (string) Carbon::parse((string) ($this->dataLettura ?: now()->toDateString()))->format('Y'), + ]; + } + + /** + * @return array> + */ + public function getRecentGeneralReadingsProperty(): array + { + if (! $this->servizioId) { + return []; + } + + return StabileServizioLettura::query() + ->where('stabile_servizio_id', (int) $this->servizioId) + ->whereNull('unita_immobiliare_id') + ->orderByDesc('periodo_al') + ->orderByDesc('id') + ->limit(6) + ->get(['id', 'periodo_al', 'lettura_fine', 'consumo_valore', 'rilevatore_nome', 'workflow_stato', 'protocollo_numero']) + ->map(fn(StabileServizioLettura $row): array => [ + 'id' => (int) $row->id, + 'date' => $row->periodo_al?->format('d/m/Y') ?: optional($row->created_at)->format('d/m/Y'), + 'value' => $row->lettura_fine, + 'consumo' => $row->consumo_valore, + 'reader' => trim((string) ($row->rilevatore_nome ?? '')), + 'workflow' => trim((string) ($row->workflow_stato ?? '')), + 'protocollo' => trim((string) ($row->protocollo_numero ?? '')), + ]) + ->all(); + } + + public function registraLetturaGenerale(): void + { + $user = Auth::user(); + if (! $user instanceof User) { + return; + } + + $validated = $this->validate([ + 'stabileId' => ['required', 'integer'], + 'servizioId' => ['required', 'integer'], + 'letturaAttuale' => ['required', 'numeric', 'min:0'], + 'dataLettura' => ['required', 'date'], + 'rilevatoreNome' => ['required', 'string', 'max:255'], + 'rilevatoreTipo' => ['nullable', 'string', 'max:80'], + 'aceaWindowStart' => ['nullable', 'date'], + 'aceaWindowEnd' => ['nullable', 'date', 'after_or_equal:aceaWindowStart'], + 'aceaVisitDate' => ['nullable', 'date'], + 'aceaVisitTimeFrom' => ['nullable', 'date_format:H:i'], + 'aceaVisitTimeTo' => ['nullable', 'date_format:H:i'], + 'noteGenerali' => ['nullable', 'string', 'max:4000'], + 'newGeneralWaterPhotos.*'=> ['nullable', 'image', 'max:8192'], + ], [ + 'servizioId.required' => 'Seleziona il contratto/servizio acqua da usare per la lettura generale.', + 'letturaAttuale.required' => 'Inserisci la lettura attuale del contatore generale.', + 'dataLettura.required' => 'Indica la data effettiva della lettura.', + 'rilevatoreNome.required' => 'Seleziona o inserisci il nominativo del letturista.', + 'aceaWindowEnd.after_or_equal' => 'La fine finestra autolettura non puo essere precedente all\'inizio.', + ]); + + $servizio = StabileServizio::query() + ->where('id', (int) $validated['servizioId']) + ->where('stabile_id', (int) $validated['stabileId']) + ->where('tipo', 'acqua') + ->where('attivo', true) + ->first(); + + if (! $servizio instanceof StabileServizio) { + Notification::make() + ->title('Contratto acqua non valido') + ->body('Controlla lo stabile e il contratto Acea selezionato prima di registrare la lettura generale.') + ->warning() + ->send(); + + return; + } + + $readingDate = Carbon::parse((string) $validated['dataLettura']); + $protocol = $this->generateGeneralReadingProtocol($readingDate); + $previous = StabileServizioLettura::query() + ->where('stabile_servizio_id', (int) $servizio->id) + ->whereNull('unita_immobiliare_id') + ->whereNotNull('lettura_fine') + ->orderByDesc('periodo_al') + ->orderByDesc('id') + ->first(); + + $letturaInizio = $previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null; + $letturaFine = round((float) $validated['letturaAttuale'], 3); + $consumo = $letturaInizio !== null ? round($letturaFine - $letturaInizio, 3) : null; + + if ($consumo !== null && $consumo < 0) { + $this->addError('letturaAttuale', 'La lettura attuale non puo essere inferiore alla precedente del contatore generale.'); + return; + } + + $storageDirectory = $this->buildGeneralWaterStorageDirectory($servizio, $readingDate); + $photoAssets = []; + + foreach ($this->newGeneralWaterPhotos as $index => $file) { + if (! is_object($file) || ! method_exists($file, 'store')) { + continue; + } + + $draftCode = $this->resolveDraftUploadCode($file, $index); + $naming = $this->buildGeneralWaterPhotoNaming($servizio, $protocol, $draftCode, (string) $file->getClientOriginalName(), $readingDate); + $stored = app(TicketAttachmentUploadService::class)->store( + $file, + $storageDirectory, + [ + 'stored_basename' => $naming['stored_basename'], + 'display_name' => $naming['display_name'], + ] + ); + $storedMetadata = app(TicketAttachmentUploadService::class)->mergeClientCaptureMetadata( + is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [], + $this->resolveClientCaptureMetadata( + (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ''), + (int) (method_exists($file, 'getSize') ? $file->getSize() : 0), + ), + ); + + $photoAssets[] = [ + 'path' => (string) ($stored['path'] ?? ''), + 'original_name' => (string) ($stored['original_name'] ?? ''), + 'metadata' => $storedMetadata, + 'description' => trim((string) ($this->generalWaterPhotoDescriptions[$index] ?? '')), + 'drive_directory' => (string) ($naming['drive_relative_directory'] ?? ''), + 'display_name' => (string) ($naming['display_name'] ?? ''), + 'draft_protocol' => $draftCode, + ]; + } + + $reference = 'TICKET-ACQUA-GENERALE:' . (int) $servizio->id . ':' . now()->format('YmdHis'); + $readerLabel = trim((string) $validated['rilevatoreNome']); + $snapshot = $this->getSelectedServiceSnapshotProperty(); + $row = StabileServizioLettura::query()->create([ + 'stabile_id' => (int) $validated['stabileId'], + 'stabile_servizio_id' => (int) $servizio->id, + 'fornitore_id' => $servizio->fornitore_id, + 'periodo_dal' => $previous?->periodo_al, + 'periodo_al' => $readingDate, + 'tipologia_lettura' => 'autolettura_contatore_generale', + 'canale_acquisizione' => 'filament_ticket_acqua_generale', + 'riferimento_acquisizione' => $reference, + 'workflow_stato' => 'ricevuta', + 'protocollo_categoria' => 'ACQ-GEN', + 'protocollo_numero' => $protocol, + 'prossima_lettura_scadenza_at' => filled($validated['aceaVisitDate'] ?? null) ? Carbon::parse((string) $validated['aceaVisitDate']) : null, + 'deadline_lettura_at' => filled($validated['aceaWindowEnd'] ?? null) + ? Carbon::parse((string) $validated['aceaWindowEnd']) + : (filled($validated['aceaVisitDate'] ?? null) ? Carbon::parse((string) $validated['aceaVisitDate']) : null), + 'rilevatore_tipo' => trim((string) ($validated['rilevatoreTipo'] ?? '')) ?: $this->inferReaderType(), + 'rilevatore_nome' => $readerLabel, + 'lettura_precedente_valore' => $letturaInizio, + 'lettura_inizio' => $letturaInizio, + 'lettura_fine' => $letturaFine, + 'lettura_foto_path' => $photoAssets[0]['path'] ?? null, + 'lettura_foto_original_name' => $photoAssets[0]['original_name'] ?? null, + 'lettura_foto_metadata' => $photoAssets[0]['metadata'] ?? null, + 'consumo_valore' => $consumo, + 'consumo_unita' => 'mc', + 'archivio_documentale_path' => $photoAssets[0]['drive_directory'] ?? null, + 'raw' => [ + 'source' => 'ticket_acqua_generale', + 'service_snapshot' => $snapshot, + 'reader_user_id' => (int) ($this->rilevatoreUserId ?? 0) ?: null, + 'reader_assignment' => [ + 'user_id' => (int) ($this->rilevatoreUserId ?? 0) ?: null, + 'name' => $readerLabel, + 'type' => $this->inferReaderType(), + ], + 'acea_schedule' => [ + 'window_start' => $validated['aceaWindowStart'] ?? null, + 'window_end' => $validated['aceaWindowEnd'] ?? null, + 'visit_date' => $validated['aceaVisitDate'] ?? null, + 'visit_time_from'=> $validated['aceaVisitTimeFrom'] ?? null, + 'visit_time_to' => $validated['aceaVisitTimeTo'] ?? null, + 'channel' => $this->aceaAutoSendChannel, + ], + 'acea_submission' => [ + 'sms_number' => '3399941808', + 'call_center' => '800 130 331', + 'sms_payload' => $snapshot['acea_sms_preview'] ?? null, + 'my_acea_url' => 'https://www.aceaato2.it', + ], + 'notes' => trim((string) ($validated['noteGenerali'] ?? '')), + 'photos' => $photoAssets, + ], + 'created_by' => (int) $user->id, + ]); + + $this->syncGeneralWaterScadenze($row, $servizio, $readerLabel); + $this->resetGeneralWaterDraftUploadState(); + $this->letturaAttuale = null; + $this->noteGenerali = null; + $this->dispatch('ticket-acqua-generale-draft-reset'); + $this->loadLastGeneralReading(); + + Notification::make() + ->title('Lettura generale registrata') + ->body('Protocollo ' . $protocol . ' salvato per il contratto selezionato.') + ->success() + ->send(); + } + + private function loadStabileOptions(): void + { + $user = Auth::user(); + if (! $user instanceof User) { + $this->stabileOptions = []; + return; + } + + $this->stabileOptions = StabileContext::accessibleStabili($user) + ->map(fn(Stabile $stabile): array => [ + 'id' => (int) $stabile->id, + 'label' => trim((string) ($stabile->codice_stabile ?? '') . ' - ' . (string) ($stabile->denominazione ?? '')), + ]) + ->values() + ->all(); + } + + private function bootstrapDefaultSelection(): void + { + $user = Auth::user(); + if (! $user instanceof User) { + return; + } + + $activeId = StabileContext::resolveActiveStabileId($user); + $optionIds = array_column($this->stabileOptions, 'id'); + if ($activeId && in_array((int) $activeId, $optionIds, true)) { + $this->stabileId = (int) $activeId; + return; + } + + $this->stabileId = $optionIds[0] ?? null; + } + + private function loadServiceOptions(): void + { + if (! $this->stabileId) { + $this->servizioOptions = []; + $this->servizioId = null; + return; + } + + $this->servizioOptions = StabileServizio::query() + ->where('stabile_id', (int) $this->stabileId) + ->where('tipo', 'acqua') + ->where('attivo', true) + ->orderBy('nome') + ->orderBy('codice_utenza') + ->get(['id', 'nome', 'codice_utenza', 'codice_cliente', 'codice_contratto', 'contatore_matricola']) + ->map(function (StabileServizio $servizio): array { + $parts = [trim((string) ($servizio->nome ?: 'Contratto acqua #' . $servizio->id))]; + if (filled($servizio->codice_contratto)) { + $parts[] = 'Contratto ' . trim((string) $servizio->codice_contratto); + } + if (filled($servizio->codice_utenza)) { + $parts[] = 'Utenza ' . trim((string) $servizio->codice_utenza); + } + if (filled($servizio->contatore_matricola)) { + $parts[] = 'Contatore ' . trim((string) $servizio->contatore_matricola); + } + + return [ + 'id' => (int) $servizio->id, + 'label' => implode(' - ', array_filter($parts)), + ]; + }) + ->values() + ->all(); + + $serviceIds = array_column($this->servizioOptions, 'id'); + if (! in_array((int) ($this->servizioId ?? 0), $serviceIds, true)) { + $this->servizioId = $serviceIds[0] ?? null; + } + } + + private function loadReaderOptions(): void + { + $user = Auth::user(); + if (! $user instanceof User || ! $this->stabileId) { + $this->readerOptions = []; + return; + } + + $stabile = Stabile::query() + ->with(['amministratore.user:id,name', 'collaboratori:id,name']) + ->find((int) $this->stabileId); + + $options = []; + $push = static function (array &$options, ?User $candidate, string $tipo): void { + if (! $candidate instanceof User) { + return; + } + + $options[(int) $candidate->id] = [ + 'id' => (int) $candidate->id, + 'label' => trim((string) $candidate->name), + 'tipo' => $tipo, + ]; + }; + + $push($options, $user, $user->hasRole('collaboratore') ? 'collaboratore' : 'amministrazione'); + $push($options, $stabile?->amministratore?->user, 'amministrazione'); + + foreach (($stabile?->collaboratori ?? collect()) as $collaboratore) { + $push($options, $collaboratore, 'collaboratore'); + } + + uasort($options, static fn(array $left, array $right): int => strcasecmp((string) $left['label'], (string) $right['label'])); + + $this->readerOptions = array_values($options); + + $readerIds = array_column($this->readerOptions, 'id'); + if (! in_array((int) ($this->rilevatoreUserId ?? 0), $readerIds, true)) { + $this->rilevatoreUserId = (int) $user->id; + $this->updatedRilevatoreUserId(); + } + } + + private function loadLastGeneralReading(): void + { + $this->lastGeneralReading = null; + + if (! $this->servizioId) { + return; + } + + $previous = StabileServizioLettura::query() + ->where('stabile_servizio_id', (int) $this->servizioId) + ->whereNull('unita_immobiliare_id') + ->whereNotNull('lettura_fine') + ->orderByDesc('periodo_al') + ->orderByDesc('id') + ->first(); + + if (! $previous instanceof StabileServizioLettura) { + return; + } + + $this->lastGeneralReading = [ + 'id' => (int) $previous->id, + 'date' => $previous->periodo_al?->format('d/m/Y'), + 'value' => $previous->lettura_fine, + 'reader' => trim((string) ($previous->rilevatore_nome ?? '')), + 'workflow' => trim((string) ($previous->workflow_stato ?? '')), + 'protocol' => trim((string) ($previous->protocollo_numero ?? '')), + ]; + } + + private function appendPendingGeneralWaterPhotos(): void + { + $pending = array_values(array_filter($this->pendingGeneralWaterPhotos, fn($file) => is_object($file))); + if ($pending === []) { + return; + } + + $available = max(0, 4 - count($this->newGeneralWaterPhotos)); + if ($available <= 0) { + $this->pendingGeneralWaterPhotos = []; + return; + } + + $accepted = array_slice($pending, 0, $available); + $currentTotal = count($this->newGeneralWaterPhotos); + $this->newGeneralWaterPhotos = array_values(array_merge($this->newGeneralWaterPhotos, $accepted)); + $this->pendingGeneralWaterPhotos = []; + $this->assignDraftUploadCodes($accepted, $currentTotal); + $this->syncGeneralWaterDescriptionSlots(); + } + + private function resetGeneralWaterDraftUploadState(): void + { + $this->newGeneralWaterPhotos = []; + $this->pendingGeneralWaterPhotos = []; + $this->generalWaterPhotoDescriptions = []; + $this->generalWaterUploadCodes = []; + $this->generalWaterClientMetadata = []; + $this->generalWaterDraftProtocol = 'TAG-' . now()->format('Ymd-His'); + $this->generalWaterDraftSequence = 1; + } + + private function syncGeneralWaterDescriptionSlots(): void + { + $next = []; + foreach ($this->newGeneralWaterPhotos as $index => $_file) { + $next[$index] = (string) ($this->generalWaterPhotoDescriptions[$index] ?? ''); + } + + $this->generalWaterPhotoDescriptions = $next; + } + + /** + * @param array $files + */ + private function assignDraftUploadCodes(array $files, int $fallbackIndexStart): void + { + foreach ($files as $offset => $file) { + if (! is_object($file)) { + continue; + } + + $key = $this->resolveUploadQueueKey($file, $fallbackIndexStart + $offset); + if (isset($this->generalWaterUploadCodes[$key])) { + continue; + } + + $this->generalWaterUploadCodes[$key] = $this->buildDraftProtocolCode($this->generalWaterDraftSequence); + $this->generalWaterDraftSequence++; + } + } + + private function resolveDraftUploadCode(object $file, int $fallbackIndex): string + { + $key = $this->resolveUploadQueueKey($file, $fallbackIndex); + + if (! isset($this->generalWaterUploadCodes[$key])) { + $this->generalWaterUploadCodes[$key] = $this->buildDraftProtocolCode($this->generalWaterDraftSequence); + $this->generalWaterDraftSequence++; + } + + return $this->generalWaterUploadCodes[$key]; + } + + private function buildDraftProtocolCode(int $sequence): string + { + return $this->generalWaterDraftProtocol . '-F' . str_pad((string) $sequence, 2, '0', STR_PAD_LEFT); + } + + private function buildDraftUploadDisplayName(string $protocol, string $originalName): string + { + $extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION)); + + return $protocol . ($extension !== '' ? '.' . $extension : ''); + } + + private function resolveUploadQueueKey(object $file, int $fallbackIndex): string + { + if (method_exists($file, 'getFilename')) { + $filename = trim((string) $file->getFilename()); + if ($filename !== '') { + return $filename; + } + } + + return spl_object_hash($file) ?: 'general-water-upload-' . $fallbackIndex; + } + + private function generateGeneralReadingProtocol(Carbon $readingDate): string + { + $year = $readingDate->format('Y'); + $next = StabileServizioLettura::query() + ->where('tipologia_lettura', 'autolettura_contatore_generale') + ->whereYear('created_at', (int) $year) + ->count() + 1; + + return 'ACQ-GEN-' . $year . '-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT); + } + + private function buildGeneralWaterStorageDirectory(StabileServizio $servizio, Carbon $readingDate): string + { + $servizio->loadMissing('stabile:id,denominazione,codice_stabile'); + + $stabileLabel = trim((string) ($servizio->stabile?->denominazione ?: $servizio->stabile?->codice_stabile ?: ('stabile-' . $servizio->stabile_id))); + + return 'condomini/' . $this->normalizeLinuxPathSegment($stabileLabel) . '/utenze/acqua/generale/' . $readingDate->format('Y'); + } + + /** + * @return array{stored_basename:string,display_name:string,drive_relative_directory:string} + */ + private function buildGeneralWaterPhotoNaming(StabileServizio $servizio, string $protocol, string $draftCode, string $originalName, Carbon $readingDate): array + { + $extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION)); + $suffix = $extension !== '' ? '.' . $extension : ''; + $serviceKey = $this->normalizeLinuxPathSegment((string) ($servizio->codice_contratto ?: $servizio->codice_utenza ?: $servizio->id)); + $base = trim(implode('_', array_filter([ + 'contatore_generale', + $serviceKey, + Str::lower(str_replace(['.', ' '], '-', $protocol)), + Str::lower(str_replace(['.', ' '], '-', $draftCode)), + ])), '_'); + + return [ + 'stored_basename' => $base !== '' ? $base : ('contatore-generale-' . $readingDate->format('YmdHis')), + 'display_name' => $protocol . '-' . $draftCode . $suffix, + 'drive_relative_directory'=> $this->buildGeneralWaterStorageDirectory($servizio, $readingDate), + ]; + } + + private function normalizeLinuxPathSegment(string $value): string + { + $ascii = (string) Str::of($value)->ascii()->lower(); + $normalized = preg_replace('/[^a-z0-9]+/', '_', $ascii) ?? ''; + + return trim($normalized, '_') !== '' ? trim($normalized, '_') : 'n_d'; + } + + /** + * @return array + */ + private function resolveClientCaptureMetadata(string $originalName, int $size): array + { + foreach ($this->generalWaterClientMetadata as $item) { + if (($item['name'] ?? '') === $originalName && (int) ($item['size'] ?? -1) === $size) { + return $item; + } + } + + foreach ($this->generalWaterClientMetadata as $item) { + if (($item['name'] ?? '') === $originalName) { + return $item; + } + } + + return []; + } + + private function inferReaderType(): string + { + foreach ($this->readerOptions as $option) { + if ((int) ($option['id'] ?? 0) === (int) ($this->rilevatoreUserId ?? 0)) { + return (string) ($option['tipo'] ?? 'amministrazione'); + } + } + + return $this->rilevatoreTipo ?: 'amministrazione'; + } + + private function syncGeneralWaterScadenze(StabileServizioLettura $reading, StabileServizio $servizio, string $readerLabel): void + { + if (! Schema::hasTable('scadenze')) { + return; + } + + $serviceName = trim((string) ($servizio->nome ?: ('Contratto #' . $servizio->id))); + $contractBits = array_filter([ + trim((string) ($servizio->codice_contratto ?? '')), + trim((string) ($servizio->codice_utenza ?? '')), + ]); + $contractLabel = $contractBits !== [] ? ' [' . implode(' / ', $contractBits) . ']' : ''; + + if (filled($this->aceaWindowStart)) { + $this->createScadenza([ + 'stabile_id' => (int) $reading->stabile_id, + 'codice_stabile_legacy' => $reading->stabile?->codice_stabile, + 'descrizione_sintetica' => 'Finestra autolettura Acea ' . $serviceName . $contractLabel . (filled($this->aceaWindowEnd) ? ' fino al ' . Carbon::parse((string) $this->aceaWindowEnd)->format('d/m/Y') : ''), + 'data_scadenza' => Carbon::parse((string) $this->aceaWindowStart)->toDateString(), + 'ora_scadenza' => null, + 'natura' => 'acqua', + 'natura_label' => 'Autolettura acqua generale', + 'gestione_tipo' => 'operativa', + 'anno' => Carbon::parse((string) ($this->dataLettura ?: now()->toDateString()))->format('Y'), + 'richiede_pop_up' => true, + 'giorni_anticipo' => 2, + 'popup_dal' => Carbon::parse((string) $this->aceaWindowStart)->copy()->subDays(2)->toDateString(), + 'legacy_payload' => [ + 'source' => 'ticket_acqua_generale', + 'schedule_type' => 'window', + 'stabile_servizio_id' => (int) $servizio->id, + 'reading_id' => (int) $reading->id, + 'reading_reference' => $reading->riferimento_acquisizione, + 'window_end' => $this->aceaWindowEnd, + 'assigned_user_id' => (int) ($this->rilevatoreUserId ?? 0) ?: null, + 'assigned_user_name' => $readerLabel, + 'sms_payload' => data_get($reading->raw, 'acea_submission.sms_payload'), + ], + ]); + } + + if (filled($this->aceaVisitDate)) { + $this->createScadenza([ + 'stabile_id' => (int) $reading->stabile_id, + 'codice_stabile_legacy' => $reading->stabile?->codice_stabile, + 'descrizione_sintetica' => 'Passaggio Acea contatore generale ' . $serviceName . $contractLabel . ' fascia ' . trim((string) $this->aceaVisitTimeFrom) . '-' . trim((string) $this->aceaVisitTimeTo), + 'data_scadenza' => Carbon::parse((string) $this->aceaVisitDate)->toDateString(), + 'ora_scadenza' => $this->aceaVisitTimeFrom, + 'natura' => 'acqua', + 'natura_label' => 'Passaggio Acea contatore generale', + 'gestione_tipo' => 'operativa', + 'anno' => Carbon::parse((string) ($this->dataLettura ?: now()->toDateString()))->format('Y'), + 'richiede_pop_up' => true, + 'giorni_anticipo' => 1, + 'popup_dal' => Carbon::parse((string) $this->aceaVisitDate)->copy()->subDay()->toDateString(), + 'legacy_payload' => [ + 'source' => 'ticket_acqua_generale', + 'schedule_type' => 'visit', + 'stabile_servizio_id' => (int) $servizio->id, + 'reading_id' => (int) $reading->id, + 'reading_reference' => $reading->riferimento_acquisizione, + 'visit_time_from' => $this->aceaVisitTimeFrom, + 'visit_time_to' => $this->aceaVisitTimeTo, + 'assigned_user_id' => (int) ($this->rilevatoreUserId ?? 0) ?: null, + 'assigned_user_name' => $readerLabel, + ], + ]); + } + } + + /** + * @param array $payload + */ + private function createScadenza(array $payload): void + { + Scadenza::query()->create($payload); + } +} \ No newline at end of file diff --git a/resources/views/filament/pages/condomini/stabile-posta.blade.php b/resources/views/filament/pages/condomini/stabile-posta.blade.php index 5686dc5..21c4dbc 100644 --- a/resources/views/filament/pages/condomini/stabile-posta.blade.php +++ b/resources/views/filament/pages/condomini/stabile-posta.blade.php @@ -39,7 +39,7 @@
@csrf - +
diff --git a/resources/views/filament/pages/contabilita/fattura-elettronica-scheda.blade.php b/resources/views/filament/pages/contabilita/fattura-elettronica-scheda.blade.php index 0637939..69c806d 100644 --- a/resources/views/filament/pages/contabilita/fattura-elettronica-scheda.blade.php +++ b/resources/views/filament/pages/contabilita/fattura-elettronica-scheda.blade.php @@ -172,7 +172,7 @@
PDF allegato (se presente) oppure prospetto generato
@if($this->documento)
- Protocollo: {{ $this->documento->numero_protocollo }} + Protocollo: {{ $this->documentoProtocolloConAnno ?: $this->documento->numero_protocollo }} · +
+ @endforeach + + @else +
Nessuna foto in coda. Aggiungi almeno la foto display e, se possibile, matricola/contratto esposto vicino al contatore.
+ @endif + + + +
+
Storico rapido contratto
+ @if(count($this->recentGeneralReadings) > 0) +
+ @foreach($this->recentGeneralReadings as $row) +
+
+
{{ $row['date'] ?: 'n/d' }} · {{ $row['protocollo'] ?: 'senza protocollo' }}
+
{{ $row['workflow'] ?: 'n/d' }}
+
+
Valore: {{ $row['value'] !== null ? number_format((float) $row['value'], 3, ',', '.') . ' mc' : 'n/d' }}
+ @if($row['consumo'] !== null) +
Consumo: {{ number_format((float) $row['consumo'], 3, ',', '.') }} mc
+ @endif + @if($row['reader'] !== '') +
Letturista: {{ $row['reader'] }}
+ @endif +
+ @endforeach +
+ @else +
Storico ancora vuoto per questo contratto.
+ @endif +
+ +
+ + Salva lettura generale + Salvataggio... + +
+ + + + \ No newline at end of file diff --git a/resources/views/filament/pages/supporto/ticket-acqua.blade.php b/resources/views/filament/pages/supporto/ticket-acqua.blade.php index 6f26673..f2df229 100644 --- a/resources/views/filament/pages/supporto/ticket-acqua.blade.php +++ b/resources/views/filament/pages/supporto/ticket-acqua.blade.php @@ -43,6 +43,7 @@ @endforeach +
Se lo stabile non ha ancora un servizio acqua attivo, preparo automaticamente un archivio letture dedicato.