*/ public array $newWaterPhotos = []; /** @var array */ public array $pendingWaterPhotos = []; /** @var array */ public array $waterPhotoDescriptions = []; /** @var array */ public array $waterUploadCodes = []; public string $waterDraftProtocol = ''; public int $waterDraftSequence = 1; /** @var array */ public array $stabileOptions = []; /** @var array */ public array $servizioOptions = []; /** @var array */ public array $unitaOptions = []; /** @var array|null */ public ?array $previousReading = null; public static function canAccess(): bool { $user = Auth::user(); return $user instanceof User && $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']); } public function mount(): void { $this->dataLettura = now()->toDateString(); $this->resetWaterDraftUploadState(); $this->loadStabileOptions(); $this->bootstrapDefaultSelection(); $this->loadServiceOptions(); $this->loadUnitOptions(); $this->loadPreviousReading(); } public function updatedStabileId(): void { $this->loadServiceOptions(); $this->loadUnitOptions(); $this->loadPreviousReading(); } public function updatedServizioId(): void { $this->loadPreviousReading(); } public function updatedUnitaId(): void { $this->loadPreviousReading(); } public function updatedPendingWaterPhotos(): void { $this->appendPendingWaterPhotos(); } public function removeWaterPhoto(int $index): void { $file = $this->newWaterPhotos[$index] ?? null; unset($this->newWaterPhotos[$index]); $this->newWaterPhotos = array_values($this->newWaterPhotos); if (is_object($file)) { unset($this->waterUploadCodes[$this->resolveUploadQueueKey($file, $index)]); } $this->syncWaterDescriptionSlots(); } /** * @return array}> */ public function getSelectedWaterUploadsProperty(): array { $rows = []; foreach ($this->newWaterPhotos 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'); $protocol = $this->resolveDraftUploadCode($file, $index); $metadata = app(TicketAttachmentUploadService::class)->inspectUpload($file); $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' => (int) (method_exists($file, 'getSize') ? $file->getSize() : 0), 'metadata' => is_array($metadata) ? $metadata : [], ]; } return $rows; } public function registraLetturaAcqua(): void { $user = Auth::user(); if (! $user instanceof User) { return; } $validated = $this->validate([ 'stabileId' => ['required', 'integer'], 'servizioId' => ['required', 'integer'], 'unitaId' => ['required', 'integer'], 'letturaAttuale' => ['required', 'numeric', 'min:0'], 'dataLettura' => ['nullable', 'date'], 'rilevatoreNome' => ['required', 'string', 'max:255'], 'rilevatoreContatto' => ['nullable', 'string', 'max:255'], 'noteGenerali' => ['nullable', 'string', 'max:2000'], 'newWaterPhotos.*' => ['nullable', 'image', 'max:8192'], ]); $stabileIds = $this->resolveAccessibleStabileIds($user); $servizio = StabileServizio::query() ->where('id', (int) $validated['servizioId']) ->where('stabile_id', (int) $validated['stabileId']) ->whereIn('stabile_id', $stabileIds) ->where('tipo', 'acqua') ->first(); $unita = UnitaImmobiliare::query() ->where('id', (int) $validated['unitaId']) ->where('stabile_id', (int) $validated['stabileId']) ->first(); if (! $servizio || ! $unita) { Notification::make() ->title('Selezione acqua non valida') ->body('Controlla stabile, servizio e unità prima di inviare la lettura.') ->warning() ->send(); return; } $previous = StabileServizioLettura::query() ->where('stabile_servizio_id', (int) $servizio->id) ->where('unita_immobiliare_id', (int) $unita->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 può essere inferiore alla precedente.'); Notification::make() ->title('Lettura non coerente') ->body('Il valore inserito è inferiore alla lettura precedente memorizzata.') ->warning() ->send(); return; } $photoAssets = []; foreach ($this->newWaterPhotos as $index => $file) { if (! is_object($file) || ! method_exists($file, 'store')) { continue; } $protocol = $this->resolveDraftUploadCode($file, $index); $stored = app(TicketAttachmentUploadService::class)->store( $file, 'ticket-acqua/' . $servizio->id . '/' . $unita->id, [ 'stored_basename' => pathinfo($this->buildDraftUploadDisplayName($protocol, (string) $file->getClientOriginalName()), PATHINFO_FILENAME), 'display_name' => $this->buildDraftUploadDisplayName($protocol, (string) $file->getClientOriginalName()), ] ); $photoAssets[] = [ 'index' => $index, 'protocol' => $protocol, 'path' => $stored['path'], 'original_name' => $stored['original_name'] ?? null, 'source_original_name' => $stored['source_original_name'] ?? null, 'mime' => $stored['mime'] ?? null, 'size' => $stored['size'] ?? null, 'metadata' => is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [], 'description' => trim((string) ($this->waterPhotoDescriptions[$index] ?? '')), ]; } $row = StabileServizioLettura::query()->create([ 'stabile_id' => (int) $servizio->stabile_id, 'stabile_servizio_id' => (int) $servizio->id, 'unita_immobiliare_id' => (int) $unita->id, 'fornitore_id' => $servizio->fornitore_id, 'periodo_dal' => $previous?->periodo_al, 'periodo_al' => filled($validated['dataLettura'] ?? null) ? Carbon::parse((string) $validated['dataLettura'])->toDateString() : now()->toDateString(), 'tipologia_lettura' => 'ticket_acqua_mobile', 'canale_acquisizione' => 'filament_ticket_acqua', 'workflow_stato' => 'ricevuta', 'riferimento_acquisizione' => 'TICKET-ACQUA:' . (int) $servizio->id . ':' . (int) $unita->id . ':' . now()->format('YmdHis'), 'rilevatore_tipo' => 'operatore_filament', 'rilevatore_nome' => trim((string) $validated['rilevatoreNome']), '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' => [ 'photos' => $photoAssets, 'contact' => trim((string) ($validated['rilevatoreContatto'] ?? '')), 'note' => trim((string) ($validated['noteGenerali'] ?? '')), ], 'consumo_valore' => $consumo, 'consumo_unita' => 'mc', 'raw' => [ 'source' => 'ticket_acqua_filament', 'contact' => trim((string) ($validated['rilevatoreContatto'] ?? '')), 'note' => trim((string) ($validated['noteGenerali'] ?? '')), 'photo_assets' => $photoAssets, ], 'created_by' => (int) $user->id, ]); $this->letturaAttuale = null; $this->noteGenerali = null; $this->dataLettura = now()->toDateString(); $this->resetWaterDraftUploadState(); $this->loadPreviousReading(); $this->dispatch('ticket-acqua-draft-reset'); Notification::make() ->title('Ticket acqua registrato') ->body('Lettura salvata per unità ' . ((string) ($unita->codice_unita ?: ('#' . $unita->id))) . ' con riferimento #' . $row->id . '.') ->success() ->send(); } private function loadStabileOptions(): void { $user = Auth::user(); if (! $user instanceof User) { $this->stabileOptions = []; return; } $this->stabileOptions = StabileContext::accessibleStabili($user) ->sortBy(fn($stabile) => mb_strtolower((string) ($stabile->denominazione ?? ''))) ->map(fn($stabile): array => [ 'id' => (int) $stabile->id, 'label' => trim((string) (($stabile->codice_stabile ?? '') !== '' ? ($stabile->codice_stabile . ' - ' . $stabile->denominazione) : ($stabile->denominazione ?? ('Stabile #' . $stabile->id)))), ]) ->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') ->get(['id', 'nome', 'codice_utenza', 'contatore_matricola']) ->map(function (StabileServizio $servizio): array { $bits = [trim((string) ($servizio->nome ?: 'Servizio acqua #' . $servizio->id))]; if (filled($servizio->codice_utenza)) { $bits[] = 'Utenza ' . trim((string) $servizio->codice_utenza); } if (filled($servizio->contatore_matricola)) { $bits[] = 'Contatore ' . trim((string) $servizio->contatore_matricola); } return [ 'id' => (int) $servizio->id, 'label' => implode(' - ', array_filter($bits)), ]; }) ->all(); $serviceIds = array_column($this->servizioOptions, 'id'); if (! in_array((int) $this->servizioId, $serviceIds, true)) { $this->servizioId = $serviceIds[0] ?? null; } } private function loadUnitOptions(): void { if (! $this->stabileId) { $this->unitaOptions = []; $this->unitaId = null; return; } $this->unitaOptions = UnitaImmobiliare::query() ->where('stabile_id', (int) $this->stabileId) ->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)))]; if (filled($unita->denominazione)) { $bits[] = trim((string) $unita->denominazione); } $location = array_filter([ filled($unita->scala) ? 'Scala ' . trim((string) $unita->scala) : null, filled($unita->piano) ? 'Piano ' . trim((string) $unita->piano) : null, filled($unita->interno) ? 'Int. ' . trim((string) $unita->interno) : null, ]); if ($location !== []) { $bits[] = implode(' ', $location); } return [ 'id' => (int) $unita->id, 'label' => implode(' - ', array_filter($bits)), ]; }) ->all(); $unitIds = array_column($this->unitaOptions, 'id'); if (! in_array((int) $this->unitaId, $unitIds, true)) { $this->unitaId = $unitIds[0] ?? null; } } private function loadPreviousReading(): void { $this->previousReading = null; if (! $this->servizioId || ! $this->unitaId) { return; } $previous = StabileServizioLettura::query() ->where('stabile_servizio_id', (int) $this->servizioId) ->where('unita_immobiliare_id', (int) $this->unitaId) ->whereNotNull('lettura_fine') ->orderByDesc('periodo_al') ->orderByDesc('id') ->first(); if (! $previous) { return; } $this->previousReading = [ 'value' => $previous->lettura_fine, 'date' => $previous->periodo_al?->format('d/m/Y'), 'id' => (int) $previous->id, ]; } private function appendPendingWaterPhotos(): void { $pending = array_values(array_filter($this->pendingWaterPhotos, fn($file) => is_object($file))); if ($pending === []) { return; } $available = max(0, 4 - count($this->newWaterPhotos)); if ($available <= 0) { $this->pendingWaterPhotos = []; Notification::make() ->title('Limite foto raggiunto') ->body('Puoi tenere in coda al massimo 4 foto per una lettura acqua.') ->warning() ->send(); return; } $accepted = array_slice($pending, 0, $available); $currentTotal = count($this->newWaterPhotos); $this->newWaterPhotos = array_values(array_merge($this->newWaterPhotos, $accepted)); $this->pendingWaterPhotos = []; $this->assignDraftUploadCodes($accepted, $currentTotal); $this->syncWaterDescriptionSlots(); } private function resetWaterDraftUploadState(): void { $this->newWaterPhotos = []; $this->pendingWaterPhotos = []; $this->waterPhotoDescriptions = []; $this->waterUploadCodes = []; $this->waterDraftProtocol = 'TA-' . now()->format('Ymd-His'); $this->waterDraftSequence = 1; } private function syncWaterDescriptionSlots(): void { $next = []; foreach ($this->newWaterPhotos as $index => $_file) { $next[$index] = (string) ($this->waterPhotoDescriptions[$index] ?? ''); } $this->waterPhotoDescriptions = $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->waterUploadCodes[$key])) { continue; } $this->waterUploadCodes[$key] = $this->buildDraftProtocolCode($this->waterDraftSequence); $this->waterDraftSequence++; } } private function resolveDraftUploadCode(object $file, int $fallbackIndex): string { $key = $this->resolveUploadQueueKey($file, $fallbackIndex); if (! isset($this->waterUploadCodes[$key])) { $this->waterUploadCodes[$key] = $this->buildDraftProtocolCode($this->waterDraftSequence); $this->waterDraftSequence++; } return $this->waterUploadCodes[$key]; } private function buildDraftProtocolCode(int $sequence): string { return $this->waterDraftProtocol . '-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) ?: 'water-upload-' . $fallbackIndex; } /** * @return array */ private function resolveAccessibleStabileIds(User $user): array { return StabileContext::accessibleStabili($user) ->pluck('id') ->map(fn($value) => (int) $value) ->filter(fn(int $value) => $value > 0) ->values() ->all(); } }