hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']) && Schema::hasTable('stabile_servizio_letture'); } public function mount(): void { $reqServizio = request()->integer('servizio'); $this->servizioFilter = $reqServizio > 0 ? $reqServizio : null; $scope = strtolower(trim((string) request()->query('scope', 'active'))); $this->archivioScope = in_array($scope, ['active', 'all'], true) ? $scope : 'active'; $user = Auth::user(); if ($this->servizioFilter && $user instanceof User) { $serviceStabileId = StabileServizio::query() ->whereKey((int) $this->servizioFilter) ->value('stabile_id'); $activeStabileId = StabileContext::resolveActiveStabileId($user); if (is_numeric($serviceStabileId) && (int) $serviceStabileId > 0 && (int) $activeStabileId > 0 && (int) $serviceStabileId !== (int) $activeStabileId) { $this->archivioScope = 'all'; } } $this->mountInteractsWithTable(); } private function resolveActiveAnnoGestione(): int { $user = Auth::user(); return AnnoGestioneContext::resolveActiveAnno($user instanceof User ? $user : null); } private function applyActiveYearFilter(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); }); }); } protected function getTableQuery(): Builder { $user = Auth::user(); if (! $user instanceof User) { return StabileServizioLettura::query()->whereRaw('1 = 0'); } $stabiliIds = StabileContext::accessibleStabili($user) ->pluck('id') ->map(fn($id) => (int) $id) ->filter(fn(int $id): bool => $id > 0) ->values(); if ($stabiliIds->isEmpty()) { return StabileServizioLettura::query()->whereRaw('1 = 0'); } $activeStabileId = StabileContext::resolveActiveStabileId($user); if ($this->archivioScope !== 'all' && ! $activeStabileId) { return StabileServizioLettura::query()->whereRaw('1 = 0'); } return StabileServizioLettura::query() ->when( $this->archivioScope === 'all', fn(Builder $q) => $q->whereIn('stabile_id', $stabiliIds->all()), fn(Builder $q) => $q->where('stabile_id', (int) $activeStabileId) ) ->tap(fn(Builder $query) => $this->applyActiveYearFilter($query)) ->when($this->servizioFilter, fn(Builder $q) => $q->where('stabile_servizio_id', (int) $this->servizioFilter)) ->with([ 'stabile:id,codice_stabile,denominazione', 'servizio:id,stabile_id,tipo,nome,contatore_matricola,meta', 'fornitore:id,ragione_sociale', 'voceSpesa:id,descrizione,tipo_gestione', 'unitaImmobiliare:id,codice_unita,denominazione,interno,scala,piano', 'fatturaElettronica:id,numero_fattura,data_fattura,totale', ]) ->orderByDesc('periodo_al') ->orderByDesc('id'); } public function table(Table $table): Table { $gestioneOptions = [ 'ordinaria' => 'Ordinaria', 'riscaldamento' => 'Riscaldamento', 'straordinaria' => 'Straordinaria', ]; $tipologiaOptions = [ 'stimata' => 'Stimata', 'effettiva' => 'Effettiva', 'manuale' => 'Manuale', 'automatica' => 'Automatica', 'rettifica' => 'Rettifica', ]; return $table ->striped() ->filters([ SelectFilter::make('servizio_tipo') ->label('Tipo servizio') ->options([ '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', ]) ->query(function (Builder $query, array $data): Builder { $value = (string) ($data['value'] ?? ''); if ($value === '') { return $query; } return $query->whereHas('servizio', fn(Builder $q) => $q->where('tipo', $value)); }), SelectFilter::make('stabile_id') ->label('Stabile') ->options(fn() => $this->getStabiliOptions()) ->searchable() ->query(function (Builder $query, array $data): Builder { $value = (int) ($data['value'] ?? 0); if ($value <= 0) { return $query; } return $query->where('stabile_id', $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([ ImageColumn::make('miniatura_foto') ->label('Foto') ->getStateUsing(fn(StabileServizioLettura $record): ?string => $this->resolveReadingPrimaryPhotoPath($record)) ->disk('public') ->square() ->size(42) ->toggleable() ->extraAttributes(['class' => 'py-2']), TextColumn::make('periodo_dal')->label('Dal')->date('d/m/Y')->sortable()->toggleable()->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('periodo_al')->label('Al')->date('d/m/Y')->sortable()->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('stabile.denominazione') ->label('Stabile') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { $stabile = $record->stabile; if (! $stabile instanceof Stabile) { return '—'; } $codice = trim((string) ($stabile->codice_stabile ?? '')); $nome = trim((string) ($stabile->denominazione ?? '')); return trim($codice . ($nome !== '' ? (' - ' . $nome) : '')) ?: ('Stabile #' . $stabile->id); }) ->limit(36) ->tooltip(function (StabileServizioLettura $record): string { $stabile = $record->stabile; if (! $stabile instanceof Stabile) { return '—'; } $codice = trim((string) ($stabile->codice_stabile ?? '')); $nome = trim((string) ($stabile->denominazione ?? '')); return trim($codice . ($nome !== '' ? (' - ' . $nome) : '')) ?: ('Stabile #' . $stabile->id); }) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: $this->archivioScope !== 'all'), TextColumn::make('servizio.nome') ->label('Servizio') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { $nome = trim((string) ($record->servizio?->nome ?? '')); if ($nome !== '') { return $nome; } $tipo = trim((string) ($record->servizio?->tipo ?? '')); return $tipo !== '' ? strtoupper($tipo) : '—'; }) ->limit(30) ->tooltip(function (StabileServizioLettura $record): string { $nome = trim((string) ($record->servizio?->nome ?? '')); if ($nome !== '') { return $nome; } $tipo = trim((string) ($record->servizio?->tipo ?? '')); return $tipo !== '' ? strtoupper($tipo) : '—'; }) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('servizio.contatore_matricola')->label('Matricola')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('unitaImmobiliare.denominazione') ->label('Attribuzione') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { return $this->resolveReadingAssignmentLabel($record); }) ->limit(34) ->tooltip(fn(StabileServizioLettura $record): string => $this->resolveReadingAssignmentLabel($record)) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('rilevatore_nome') ->label('Letturista / nominativo') ->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—') ->limit(28) ->tooltip(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('consumo_valore') ->label('Consumo') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { $val = $record->consumo_valore; if ($val === null) { return '—'; } $unit = trim((string) ($record->consumo_unita ?? '')); return trim((string) $val) . ($unit !== '' ? (' ' . $unit) : ''); }) ->sortable() ->extraAttributes(['class' => 'text-[11px] leading-tight font-medium']), TextColumn::make('importo_totale')->label('Importo')->money('EUR')->sortable()->toggleable()->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('tipologia_lettura') ->label('Tipo lettura') ->formatStateUsing(fn($state) => $tipologiaOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—')) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('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', 'pdf_ocr' => 'PDF/OCR', 'm_bus', 'mbus' => 'Contatore elettronico', default => $v !== '' ? strtoupper($v) : '—', }; }) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('workflow_stato') ->label('Workflow') ->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? (string) $state : 'Acquisita') ->badge() ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('raw.consistency_check.summary') ->label('Check lettura') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { $summary = trim((string) data_get($record->raw, 'consistency_check.summary', '')); return $summary !== '' ? $summary : '—'; }) ->badge() ->color(function ($state, StabileServizioLettura $record): string { return match ((string) data_get($record->raw, 'consistency_check.status', '')) { 'warning' => 'warning', 'danger' => 'danger', default => 'gray', }; }) ->limit(28) ->tooltip(function (StabileServizioLettura $record): string { $summary = trim((string) data_get($record->raw, 'consistency_check.summary', '')); return $summary !== '' ? $summary : '—'; }) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('lettura_foto_original_name') ->label('Foto') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { $photos = is_array(data_get($record->lettura_foto_metadata, 'photos')) ? data_get($record->lettura_foto_metadata, 'photos') : []; if ($photos !== []) { return count($photos) . ' foto'; } return filled($record->lettura_foto_path) ? '1 foto' : '—'; }) ->tooltip(function (StabileServizioLettura $record): string { $photos = $this->getReadingPhotoEntries($record); if ($photos === []) { return 'Nessuna foto'; } return implode("\n", array_map( fn(array $photo): string => (string) ($photo['name'] ?? basename((string) ($photo['path'] ?? 'foto'))), $photos )); }) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('protocollo_numero') ->label('Protocollo') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { $numero = trim((string) ($state ?? '')); $categoria = trim((string) ($record->protocollo_categoria ?? '')); if ($numero === '' && $categoria === '') { return '—'; } return trim($categoria . ($numero !== '' ? (' #' . $numero) : '')); }) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('prossima_lettura_scadenza_at') ->label('Scadenza prossima lettura') ->dateTime('d/m/Y H:i') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('deadline_lettura_at') ->label('Deadline campagna') ->dateTime('d/m/Y H:i') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('sollecito_inviato_at') ->label('Sollecito') ->dateTime('d/m/Y H:i') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('rilevatore_tipo') ->label('Rilevatore') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('lettura_precedente_valore') ->label('Lettura precedente') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']), TextColumn::make('archivio_documentale_path') ->label('Archivio documentale') ->limit(36) ->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('fatturaElettronica.numero_fattura') ->label('Fattura FE') ->formatStateUsing(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(), TextColumn::make('voceSpesa.descrizione') ->label('Voce spesa') ->limit(30) ->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('voceSpesa.tipo_gestione') ->label('Gestione') ->formatStateUsing(fn($state) => $gestioneOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—')) ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('fornitore.ragione_sociale') ->label('Fornitore') ->limit(30) ->tooltip(fn($state): string => trim((string) ($state ?? '')) !== '' ? trim((string) $state) : '—') ->extraAttributes(['class' => 'text-[11px] leading-tight']) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('created_at')->label('Creato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true)->extraAttributes(['class' => 'text-[11px] leading-tight']), ]) ->headerActions([ Action::make('toggleScope') ->label(fn(): string => $this->archivioScope === 'all' ? 'Vista: tutti gli stabili' : 'Vista: stabile attivo') ->icon('heroicon-o-building-office-2') ->color(fn(): string => $this->archivioScope === 'all' ? 'primary' : 'gray') ->action(function (): void { $this->archivioScope = $this->archivioScope === 'all' ? 'active' : 'all'; }), Action::make('programmaCampagnaAcqua') ->label('Campagna letture acqua') ->icon('heroicon-o-megaphone') ->color('primary') ->form([ Select::make('stabile_servizio_id') ->label('Servizio acqua') ->options(fn() => $this->getServiziOptionsByTipo('acqua')) ->searchable() ->required(), DatePicker::make('deadline_lettura_at') ->label('Scadenza invio lettura') ->native(false) ->required(), Textarea::make('messaggio_template') ->label('Messaggio base') ->default('Inviare la lettura del contatore acqua entro la data indicata allegando foto del contatore.') ->rows(3), ]) ->action(function (array $data): void { $created = $this->scheduleWaterCampaign($data); Notification::make()->title('Campagna letture impostata')->body('Righe aggiornate o create: ' . $created)->success()->send(); }), Action::make('sollecitaScadute') ->label('Sollecita scadute') ->icon('heroicon-o-bell-alert') ->color('warning') ->requiresConfirmation() ->action(function (): void { $count = $this->markOverdueWaterReminders(); Notification::make()->title('Solleciti aggiornati')->body('Righe sollecitate: ' . $count)->success()->send(); }), Action::make('create') ->label('Nuova') ->icon('heroicon-o-plus') ->form([ Select::make('stabile_servizio_id') ->label('Servizio') ->options(fn() => $this->getServiziOptions()) ->searchable() ->required(), Select::make('voce_spesa_id') ->label('Voce spesa') ->options(fn() => $this->getVociSpesaOptions()) ->searchable(), Select::make('fornitore_id') ->label('Fornitore') ->options(fn() => $this->getFornitoriOptions()) ->searchable(), Select::make('unita_immobiliare_id') ->label('Unità immobiliare') ->options(fn() => $this->getUnitaOptions()) ->searchable(), DatePicker::make('periodo_dal')->label('Periodo dal'), DatePicker::make('periodo_al')->label('Periodo al'), Select::make('tipologia_lettura') ->label('Tipologia lettura') ->options($tipologiaOptions), Select::make('canale_acquisizione') ->label('Canale acquisizione') ->options([ 'manuale' => 'Manuale', 'email' => 'Email', 'whatsapp' => 'WhatsApp', 'import_file' => 'Import file', 'm_bus' => 'Contatore elettronico', 'pdf_ocr' => 'PDF/OCR', ]), Select::make('workflow_stato') ->label('Workflow') ->options([ 'acquisita' => 'Acquisita', 'da_richiedere' => 'Da richiedere', 'richiesta_inviata' => 'Richiesta inviata', 'ricevuta' => 'Ricevuta', 'protocollata' => 'Protocollata', 'archiviata' => 'Archiviata', ]) ->default('acquisita'), TextInput::make('protocollo_categoria')->label('Categoria protocollo')->maxLength(40), TextInput::make('protocollo_numero')->label('Numero protocollo')->maxLength(50), TextInput::make('riferimento_acquisizione')->label('Riferimento acquisizione')->maxLength(191), DatePicker::make('richiesta_lettura_inviata_at')->label('Richiesta inviata il')->native(false), DatePicker::make('prossima_lettura_scadenza_at')->label('Prossima scadenza lettura')->native(false), DatePicker::make('deadline_lettura_at')->label('Deadline lettura')->native(false), Select::make('rilevatore_tipo') ->label('Rilevatore') ->options([ 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'letturista' => 'Letturista', 'amministrazione' => 'Amministrazione', ]), TextInput::make('rilevatore_nome')->label('Nome rilevatore')->maxLength(191), FileUpload::make('lettura_foto_upload') ->label('Foto contatore') ->image() ->imageEditor(), FileUpload::make('lettura_precedente_foto_upload') ->label('Foto contatore precedente') ->image(), TextInput::make('archivio_documentale_path')->label('Path archivio documentale')->maxLength(500), TextInput::make('lettura_precedente_valore')->label('Lettura precedente')->numeric(), TextInput::make('lettura_inizio')->label('Lettura inizio')->numeric(), TextInput::make('lettura_fine')->label('Lettura fine')->numeric(), TextInput::make('consumo_valore')->label('Consumo valore')->numeric(), TextInput::make('consumo_unita')->label('Unità')->maxLength(16), TextInput::make('importo_totale')->label('Importo totale')->numeric(), ]) ->action(function (array $data): void { $user = Auth::user(); if (! $user instanceof User) { return; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return; } $record = StabileServizioLettura::query()->create([ 'stabile_id' => (int) $stabileId, 'stabile_servizio_id' => (int) ($data['stabile_servizio_id'] ?? 0), 'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null, 'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null, 'unita_immobiliare_id' => (int) ($data['unita_immobiliare_id'] ?? 0) ?: null, 'periodo_dal' => $data['periodo_dal'] ?? null, 'periodo_al' => $data['periodo_al'] ?? null, 'tipologia_lettura' => $data['tipologia_lettura'] ?? null, 'canale_acquisizione' => trim((string) ($data['canale_acquisizione'] ?? '')) ?: 'manuale', 'workflow_stato' => trim((string) ($data['workflow_stato'] ?? '')) ?: 'acquisita', 'protocollo_categoria' => trim((string) ($data['protocollo_categoria'] ?? '')) ?: null, 'protocollo_numero' => trim((string) ($data['protocollo_numero'] ?? '')) ?: null, 'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null, 'richiesta_lettura_inviata_at' => $data['richiesta_lettura_inviata_at'] ?? null, 'prossima_lettura_scadenza_at' => $data['prossima_lettura_scadenza_at'] ?? null, 'deadline_lettura_at' => $data['deadline_lettura_at'] ?? null, 'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: null, 'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null, 'archivio_documentale_path' => trim((string) ($data['archivio_documentale_path'] ?? '')) ?: null, 'lettura_precedente_valore' => $data['lettura_precedente_valore'] ?? null, 'lettura_inizio' => $data['lettura_inizio'] ?? null, 'lettura_fine' => $data['lettura_fine'] ?? null, 'consumo_valore' => $data['consumo_valore'] ?? null, 'consumo_unita' => (string) ($data['consumo_unita'] ?? ''), 'importo_totale' => $data['importo_totale'] ?? null, 'created_by' => (int) $user->id, ]); $this->hydrateReadingWithPreviousData($record); $this->storeReadingUploads($record, $data); }), ]) ->actions([ Action::make('apriFoto') ->label('Foto') ->icon('heroicon-o-photo') ->button() ->color('gray') ->modalHeading('Foto contatore') ->modalWidth('5xl') ->modalSubmitAction(false) ->modalCancelActionLabel('Chiudi') ->modalContent(function (StabileServizioLettura $record): View { return view('filament.pages.condomini.partials.lettura-servizio-foto-modal', [ 'record' => $record, 'photos' => $this->getReadingPhotoEntries($record), 'mapUrl' => $this->resolveReadingMapUrl($record), 'assigned' => $this->resolveReadingAssignmentLabel($record), ]); }) ->visible(fn(StabileServizioLettura $record): bool => $this->getReadingPhotoEntries($record) !== []), Action::make('mappaFoto') ->label('Mappa') ->icon('heroicon-o-map') ->button() ->color('gray') ->url(fn(StabileServizioLettura $record): ?string => $this->resolveReadingMapUrl($record)) ->openUrlInNewTab() ->visible(fn(StabileServizioLettura $record): bool => $this->resolveReadingMapUrl($record) !== null), Action::make('apriFatturaFe') ->label('FE') ->icon('heroicon-o-receipt-percent') ->button() ->color('primary') ->url(fn(StabileServizioLettura $record): ?string => $record->fattura_elettronica_id ? FatturaElettronicaScheda::getUrl(['record' => (int) $record->fattura_elettronica_id], panel : 'admin-filament') : null) ->openUrlInNewTab() ->visible(fn(StabileServizioLettura $record): bool => (int) ($record->fattura_elettronica_id ?? 0) > 0), Action::make('riattribuisci') ->label('Riattribuisci') ->icon('heroicon-o-user-circle') ->button() ->color('gray') ->form([ Select::make('unita_immobiliare_id') ->label('Unità / nominativo') ->options(fn() => $this->getUnitaOptions()) ->searchable() ->placeholder('Contatore generale / bene comune dello stabile'), Select::make('rilevatore_tipo') ->label('Tipo rilevatore') ->options($this->getReaderTypeOptions()), TextInput::make('rilevatore_nome')->label('Nominativo rilevatore')->maxLength(191), Textarea::make('motivo')->label('Nota riassegnazione')->rows(3)->maxLength(1000), ]) ->fillForm(fn(StabileServizioLettura $record): array=> [ 'unita_immobiliare_id' => $record->unita_immobiliare_id, 'rilevatore_tipo' => (string) ($record->rilevatore_tipo ?? ''), 'rilevatore_nome' => (string) ($record->rilevatore_nome ?? ''), 'motivo' => '', ]) ->action(function (StabileServizioLettura $record, array $data): void { $raw = is_array($record->raw ?? null) ? $record->raw : []; $reassignments = is_array($raw['reassignments'] ?? null) ? $raw['reassignments'] : []; $reassignments[] = [ 'from_unita_immobiliare_id' => (int) ($record->getOriginal('unita_immobiliare_id') ?? 0) ?: null, 'to_unita_immobiliare_id' => (int) ($data['unita_immobiliare_id'] ?? 0) ?: null, 'from_reader_name' => (string) ($record->getOriginal('rilevatore_nome') ?? ''), 'to_reader_name' => trim((string) ($data['rilevatore_nome'] ?? '')), 'note' => trim((string) ($data['motivo'] ?? '')), 'changed_by' => Auth::id(), 'changed_at' => now()->toIso8601String(), ]; $raw['reassignments'] = $reassignments; $raw['reader_assignment'] = [ 'user_id' => null, 'name' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null, 'type' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: null, ]; $record->fill([ 'unita_immobiliare_id' => (int) ($data['unita_immobiliare_id'] ?? 0) ?: null, 'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: null, 'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null, 'lettura_precedente_valore' => null, 'lettura_precedente_foto_path' => null, 'raw' => $raw, ]); $record->save(); $this->hydrateReadingWithPreviousData($record); Notification::make() ->title('Attribuzione aggiornata') ->body('La lettura è stata riallineata al nuovo nominativo/unità.') ->success() ->send(); }), Action::make('spostaContatoreComune') ->label('Sposta a contatore comune') ->icon('heroicon-o-arrow-path-rounded-square') ->button() ->color('warning') ->form([ Select::make('stabile_servizio_id') ->label('Contatore / servizio comune di destinazione') ->options(fn(StabileServizioLettura $record) => $this->getCommonWaterServiceOptions((int) $record->stabile_id)) ->searchable() ->required(), Select::make('rilevatore_tipo') ->label('Tipo rilevatore') ->options($this->getReaderTypeOptions()), TextInput::make('rilevatore_nome')->label('Nominativo rilevatore')->maxLength(191), Textarea::make('motivo')->label('Motivo spostamento')->rows(3)->maxLength(1000), ]) ->fillForm(fn(StabileServizioLettura $record): array=> [ 'stabile_servizio_id' => $record->stabile_servizio_id, 'rilevatore_tipo' => (string) ($record->rilevatore_tipo ?? ''), 'rilevatore_nome' => (string) ($record->rilevatore_nome ?? ''), 'motivo' => '', ]) ->visible(fn(StabileServizioLettura $record): bool => strtolower(trim((string) ($record->servizio?->tipo ?? ''))) === 'acqua') ->action(function (StabileServizioLettura $record, array $data): void { $targetServiceId = (int) ($data['stabile_servizio_id'] ?? 0); $targetService = StabileServizio::query() ->where('id', $targetServiceId) ->where('stabile_id', (int) $record->stabile_id) ->where('tipo', 'acqua') ->first(); if (! $targetService instanceof StabileServizio) { Notification::make()->title('Servizio comune non valido')->danger()->send(); return; } $raw = is_array($record->raw ?? null) ? $record->raw : []; $movements = is_array($raw['common_meter_movements'] ?? null) ? $raw['common_meter_movements'] : []; $movements[] = [ 'from_stabile_servizio_id' => (int) ($record->getOriginal('stabile_servizio_id') ?? 0) ?: null, 'to_stabile_servizio_id' => (int) $targetService->id, 'from_unita_immobiliare_id' => (int) ($record->getOriginal('unita_immobiliare_id') ?? 0) ?: null, 'to_unita_immobiliare_id' => null, 'note' => trim((string) ($data['motivo'] ?? '')), 'changed_by' => Auth::id(), 'changed_at' => now()->toIso8601String(), ]; $raw['common_meter_movements'] = $movements; $raw['reader_assignment'] = [ 'user_id' => null, 'name' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: (trim((string) ($record->rilevatore_nome ?? '')) ?: null), 'type' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: (trim((string) ($record->rilevatore_tipo ?? '')) ?: null), ]; $raw['moved_to_common_meter'] = [ 'service_id' => (int) $targetService->id, 'service_name' => (string) ($targetService->nome ?? ''), 'common_asset' => (string) data_get($targetService->meta, 'common_asset_label', ''), 'counter_scope' => (string) data_get($targetService->meta, 'counter_scope', ''), ]; $record->fill([ 'stabile_servizio_id' => (int) $targetService->id, 'unita_immobiliare_id' => null, 'fornitore_id' => $targetService->fornitore_id, 'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: $record->rilevatore_tipo, 'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: $record->rilevatore_nome, 'tipologia_lettura' => 'autolettura_contatore_generale', 'lettura_precedente_valore' => null, 'lettura_precedente_foto_path' => null, 'raw' => $raw, ]); $record->save(); $this->hydrateReadingWithPreviousData($record); Notification::make() ->title('Lettura spostata su contatore comune') ->body('La lettura è ora agganciata al servizio corretto del contatore comune.') ->success() ->send(); }), Action::make('edit') ->label('Modifica') ->icon('heroicon-o-pencil-square') ->button() ->color('gray') ->form([ Select::make('stabile_servizio_id') ->label('Servizio') ->options(fn() => $this->getServiziOptions()) ->searchable() ->required(), Select::make('voce_spesa_id') ->label('Voce spesa') ->options(fn() => $this->getVociSpesaOptions()) ->searchable(), Select::make('fornitore_id') ->label('Fornitore') ->options(fn() => $this->getFornitoriOptions()) ->searchable(), Select::make('unita_immobiliare_id') ->label('Unità immobiliare') ->options(fn() => $this->getUnitaOptions()) ->searchable(), DatePicker::make('periodo_dal')->label('Periodo dal'), DatePicker::make('periodo_al')->label('Periodo al'), Select::make('tipologia_lettura') ->label('Tipologia lettura') ->options($tipologiaOptions), Select::make('canale_acquisizione') ->label('Canale acquisizione') ->options([ 'manuale' => 'Manuale', 'email' => 'Email', 'whatsapp' => 'WhatsApp', 'import_file' => 'Import file', 'm_bus' => 'Contatore elettronico', 'pdf_ocr' => 'PDF/OCR', ]), TextInput::make('riferimento_acquisizione')->label('Riferimento acquisizione')->maxLength(191), DatePicker::make('deadline_lettura_at')->label('Deadline lettura')->native(false), Select::make('rilevatore_tipo') ->label('Rilevatore') ->options([ 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'letturista' => 'Letturista', 'amministrazione' => 'Amministrazione', ]), TextInput::make('rilevatore_nome')->label('Nome rilevatore')->maxLength(191), FileUpload::make('lettura_foto_upload') ->label('Foto contatore') ->image() ->imageEditor(), FileUpload::make('lettura_precedente_foto_upload') ->label('Foto contatore precedente') ->image(), TextInput::make('lettura_inizio')->label('Lettura inizio')->numeric(), TextInput::make('lettura_precedente_valore')->label('Lettura precedente')->numeric(), TextInput::make('lettura_fine')->label('Lettura fine')->numeric(), TextInput::make('consumo_valore')->label('Consumo valore')->numeric(), TextInput::make('consumo_unita')->label('Unità')->maxLength(16), TextInput::make('importo_totale')->label('Importo totale')->numeric(), ]) ->fillForm(fn(StabileServizioLettura $record): array=> [ 'stabile_servizio_id' => $record->stabile_servizio_id, 'voce_spesa_id' => $record->voce_spesa_id, 'fornitore_id' => $record->fornitore_id, 'unita_immobiliare_id' => $record->unita_immobiliare_id, 'periodo_dal' => $record->periodo_dal, 'periodo_al' => $record->periodo_al, 'tipologia_lettura' => (string) ($record->tipologia_lettura ?? ''), 'canale_acquisizione' => (string) ($record->canale_acquisizione ?? ''), 'workflow_stato' => (string) ($record->workflow_stato ?? ''), 'protocollo_categoria' => (string) ($record->protocollo_categoria ?? ''), 'protocollo_numero' => (string) ($record->protocollo_numero ?? ''), 'riferimento_acquisizione' => (string) ($record->riferimento_acquisizione ?? ''), 'richiesta_lettura_inviata_at' => $record->richiesta_lettura_inviata_at, 'prossima_lettura_scadenza_at' => $record->prossima_lettura_scadenza_at, 'deadline_lettura_at' => $record->deadline_lettura_at, 'rilevatore_tipo' => (string) ($record->rilevatore_tipo ?? ''), 'rilevatore_nome' => (string) ($record->rilevatore_nome ?? ''), 'archivio_documentale_path' => (string) ($record->archivio_documentale_path ?? ''), '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 ?? ''), 'importo_totale' => $record->importo_totale, ]) ->action(function (StabileServizioLettura $record, array $data): void { $record->fill([ 'stabile_servizio_id' => (int) ($data['stabile_servizio_id'] ?? 0), 'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null, 'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null, 'unita_immobiliare_id' => (int) ($data['unita_immobiliare_id'] ?? 0) ?: null, 'periodo_dal' => $data['periodo_dal'] ?? null, 'periodo_al' => $data['periodo_al'] ?? null, 'tipologia_lettura' => $data['tipologia_lettura'] ?? null, 'canale_acquisizione' => trim((string) ($data['canale_acquisizione'] ?? '')) ?: null, 'workflow_stato' => trim((string) ($data['workflow_stato'] ?? '')) ?: null, 'protocollo_categoria' => trim((string) ($data['protocollo_categoria'] ?? '')) ?: null, 'protocollo_numero' => trim((string) ($data['protocollo_numero'] ?? '')) ?: null, 'riferimento_acquisizione' => trim((string) ($data['riferimento_acquisizione'] ?? '')) ?: null, 'richiesta_lettura_inviata_at' => $data['richiesta_lettura_inviata_at'] ?? null, 'prossima_lettura_scadenza_at' => $data['prossima_lettura_scadenza_at'] ?? null, 'deadline_lettura_at' => $data['deadline_lettura_at'] ?? null, 'rilevatore_tipo' => trim((string) ($data['rilevatore_tipo'] ?? '')) ?: null, 'rilevatore_nome' => trim((string) ($data['rilevatore_nome'] ?? '')) ?: null, 'archivio_documentale_path' => trim((string) ($data['archivio_documentale_path'] ?? '')) ?: null, 'lettura_precedente_valore' => $data['lettura_precedente_valore'] ?? null, 'lettura_inizio' => $data['lettura_inizio'] ?? null, 'lettura_fine' => $data['lettura_fine'] ?? null, 'consumo_valore' => $data['consumo_valore'] ?? null, 'consumo_unita' => (string) ($data['consumo_unita'] ?? ''), 'importo_totale' => $data['importo_totale'] ?? null, ]); $record->save(); $this->hydrateReadingWithPreviousData($record); $this->storeReadingUploads($record, $data); }), Action::make('delete') ->label('Elimina') ->icon('heroicon-o-trash') ->button() ->color('danger') ->requiresConfirmation() ->action(fn(StabileServizioLettura $record) => $record->delete()), ]); } /** * @return array */ private function getServiziOptions(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return []; } return StabileServizio::query() ->where('stabile_id', (int) $stabileId) ->orderBy('tipo') ->orderBy('nome') ->get(['id', 'tipo', 'nome', 'contatore_matricola', 'meta']) ->mapWithKeys(function (StabileServizio $s): array { $tipo = strtoupper(trim((string) ($s->tipo ?? ''))); $nome = trim((string) ($s->nome ?? '')); $matr = trim((string) ($s->contatore_matricola ?? '')); $asset = trim((string) data_get($s->meta, 'common_asset_label', '')); $label = $nome !== '' ? $nome : ($tipo !== '' ? $tipo : ('Servizio #' . $s->id)); if ($asset !== '' && ! str_contains(mb_strtolower($label), mb_strtolower($asset))) { $label .= ' - ' . $asset; } if ($matr !== '') { $label .= ' (' . $matr . ')'; } return [(int) $s->id => $label]; }) ->all(); } /** * @return array */ private function getStabiliOptions(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } return StabileContext::accessibleStabili($user) ->mapWithKeys(function (Stabile $stabile): array { $codice = trim((string) ($stabile->codice_stabile ?? '')); $nome = trim((string) ($stabile->denominazione ?? '')); $label = trim($codice . ($nome !== '' ? (' - ' . $nome) : '')) ?: ('Stabile #' . $stabile->id); return [(int) $stabile->id => $label]; }) ->all(); } /** * @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(); } /** * @return array */ private function getUnitaOptions(): array { $user = Auth::user(); if (! $user instanceof User) { return []; } $stabileId = StabileContext::resolveActiveStabileId($user); if (! $stabileId) { return []; } $units = UnitaImmobiliare::query() ->where('stabile_id', (int) $stabileId) ->orderBy('codice_unita') ->orderBy('interno') ->orderBy('id') ->get(['id', 'codice_unita', 'denominazione', 'interno']) ->all(); $latestNominativi = []; if ($units !== [] && Schema::hasTable('unita_immobiliare_nominativi')) { $rows = DB::table('unita_immobiliare_nominativi') ->whereIn('unita_immobiliare_id', array_map(fn(UnitaImmobiliare $unit): int => (int) $unit->id, $units)) ->orderByDesc('updated_at') ->orderByDesc('id') ->get(['unita_immobiliare_id', 'nominativo']); foreach ($rows as $row) { $unitId = (int) ($row->unita_immobiliare_id ?? 0); if ($unitId > 0 && ! isset($latestNominativi[$unitId])) { $latestNominativi[$unitId] = trim((string) ($row->nominativo ?? '')); } } } $options = collect($units) ->mapWithKeys(function (UnitaImmobiliare $u) use ($latestNominativi): array { $cod = trim((string) ($u->codice_unita ?? '')); $den = trim((string) ($u->denominazione ?? '')); $int = trim((string) ($u->interno ?? '')); $nom = trim((string) ($latestNominativi[(int) $u->id] ?? '')); $label = $cod !== '' ? $cod : ('UI #' . $u->id); if ($den !== '') { $label .= ' - ' . $den; } if ($int !== '') { $label .= ' (Int. ' . $int . ')'; } if ($nom !== '') { $label .= ' · ' . $nom; } return [(int) $u->id => $label]; }) ->all(); uasort($options, static fn(string $left, string $right): int => strnatcasecmp($left, $right)); return $options; } /** * @return array */ private function getReaderTypeOptions(): array { return [ 'condomino' => 'Condomino', 'inquilino' => 'Inquilino', 'letturista' => 'Letturista', 'collaboratore' => 'Collaboratore', 'amministrazione' => 'Amministrazione', 'operatore_filament' => 'Operatore Filament', 'link_pubblico' => 'Link pubblico', ]; } /** * @return array */ private function getServiziOptionsByTipo(string $tipo): array { return StabileServizio::query() ->where('tipo', $tipo) ->whereIn('id', array_keys($this->getServiziOptions())) ->orderBy('nome') ->get(['id', 'nome', 'contatore_matricola']) ->mapWithKeys(function (StabileServizio $servizio): array { $label = trim((string) ($servizio->nome ?? '')) ?: ('Servizio #' . (int) $servizio->id); if (trim((string) ($servizio->contatore_matricola ?? '')) !== '') { $label .= ' (' . trim((string) $servizio->contatore_matricola) . ')'; } return [(int) $servizio->id => $label]; }) ->all(); } /** * @return array */ private function getCommonWaterServiceOptions(int $stabileId): array { return StabileServizio::query() ->where('stabile_id', $stabileId) ->where('tipo', 'acqua') ->where('attivo', true) ->orderBy('nome') ->orderBy('contatore_matricola') ->get(['id', 'nome', 'contatore_matricola', 'meta']) ->mapWithKeys(function (StabileServizio $service): array { $name = trim((string) ($service->nome ?? '')) ?: ('Servizio #' . (int) $service->id); $asset = trim((string) data_get($service->meta, 'common_asset_label', '')); $servedArea = trim((string) data_get($service->meta, 'served_area_label', '')); $counterScope = trim((string) data_get($service->meta, 'counter_scope', '')); $matricola = trim((string) ($service->contatore_matricola ?? '')); $parts = array_filter([ $name, $asset, $servedArea, $counterScope !== '' ? 'Contatore ' . $counterScope : null, $matricola !== '' ? 'Matricola ' . $matricola : null, ]); return [(int) $service->id => implode(' - ', $parts)]; }) ->all(); } private function scheduleWaterCampaign(array $data): int { $user = Auth::user(); $stabileId = $user instanceof User ? (int) StabileContext::resolveActiveStabileId($user) : 0; $servizioId = (int) ($data['stabile_servizio_id'] ?? 0); if ($stabileId <= 0 || $servizioId <= 0) { return 0; } $deadline = $data['deadline_lettura_at'] ?? null; $message = trim((string) ($data['messaggio_template'] ?? '')); $count = 0; $unitaIds = UnitaImmobiliare::query() ->where('stabile_id', $stabileId) ->pluck('id'); foreach ($unitaIds as $unitaId) { $row = StabileServizioLettura::query() ->where('stabile_id', $stabileId) ->where('stabile_servizio_id', $servizioId) ->where('unita_immobiliare_id', (int) $unitaId) ->whereIn('workflow_stato', ['da_richiedere', 'richiesta_inviata', 'richiesta_sollecitata']) ->latest('id') ->first(); if (! $row instanceof StabileServizioLettura) { $row = StabileServizioLettura::query()->create([ 'stabile_id' => $stabileId, 'stabile_servizio_id' => $servizioId, 'unita_immobiliare_id' => (int) $unitaId, 'workflow_stato' => 'richiesta_inviata', 'canale_acquisizione' => 'manuale', 'richiesta_lettura_inviata_at' => now(), 'deadline_lettura_at' => $deadline, 'raw' => ['campagna_acqua' => ['messaggio' => $message, 'creata_da' => Auth::id()]], 'created_by' => Auth::id(), ]); } else { $raw = is_array($row->raw ?? null) ? $row->raw : []; $raw['campagna_acqua'] = [ 'messaggio' => $message, 'aggiornata_da' => Auth::id(), 'updated_at' => now()->toIso8601String(), ]; $row->workflow_stato = 'richiesta_inviata'; $row->richiesta_lettura_inviata_at = $row->richiesta_lettura_inviata_at ?? now(); $row->deadline_lettura_at = $deadline; $row->raw = $raw; $row->save(); } $count++; } return $count; } private function markOverdueWaterReminders(): int { $user = Auth::user(); $stabileId = $user instanceof User ? (int) StabileContext::resolveActiveStabileId($user) : 0; if ($stabileId <= 0) { return 0; } $rows = StabileServizioLettura::query() ->where('stabile_id', $stabileId) ->whereHas('servizio', fn(Builder $q) => $q->where('tipo', 'acqua')) ->whereNotNull('deadline_lettura_at') ->where('deadline_lettura_at', '<', now()) ->where(function (Builder $q): void { $q->whereNull('lettura_fine')->orWhereNull('consumo_valore'); }) ->get(); foreach ($rows as $row) { $raw = is_array($row->raw ?? null) ? $row->raw : []; $campagna = is_array($raw['campagna_acqua'] ?? null) ? $raw['campagna_acqua'] : []; $campagna['solleciti'] = (int) ($campagna['solleciti'] ?? 0) + 1; $campagna['ultimo_sollecito_at'] = now()->toIso8601String(); $raw['campagna_acqua'] = $campagna; $row->sollecito_inviato_at = now(); $row->workflow_stato = 'richiesta_sollecitata'; $row->raw = $raw; $row->save(); } return $rows->count(); } private function hydrateReadingWithPreviousData(StabileServizioLettura $record): void { $previous = StabileServizioLettura::query() ->where('stabile_servizio_id', (int) $record->stabile_servizio_id) ->when( $record->unita_immobiliare_id === null, fn(Builder $query) => $query->whereNull('unita_immobiliare_id'), fn(Builder $query) => $query->where('unita_immobiliare_id', (int) $record->unita_immobiliare_id) ) ->where('id', '!=', (int) $record->id) ->latest('periodo_al') ->latest('id') ->first(); if (! $previous instanceof StabileServizioLettura) { return; } $changed = false; if ($record->lettura_precedente_valore === null && $previous->lettura_fine !== null) { $record->lettura_precedente_valore = $previous->lettura_fine; $changed = true; } if (! $record->lettura_precedente_foto_path && $previous->lettura_foto_path) { $record->lettura_precedente_foto_path = $previous->lettura_foto_path; $changed = true; } if ($changed) { $record->save(); } } private function storeReadingUploads(StabileServizioLettura $record, array $data): void { $updated = false; foreach ([ 'lettura_foto_upload' => ['field' => 'lettura_foto_path', 'prefix' => 'lettura'], 'lettura_precedente_foto_upload' => ['field' => 'lettura_precedente_foto_path', 'prefix' => 'precedente'], ] as $input => $meta) { $upload = $data[$input] ?? null; $stored = $this->storeReadingPhoto($upload, $record, (string) $meta['prefix']); if (! is_array($stored)) { continue; } $record->{$meta['field']} = $stored['path']; $raw = is_array($record->raw ?? null) ? $record->raw : []; $raw[$input . '_metadata'] = $stored['metadata']; if ($input === 'lettura_foto_upload') { $record->lettura_foto_original_name = (string) ($stored['original_name'] ?? ''); $record->lettura_foto_metadata = $stored['metadata']; } $record->raw = $raw; $updated = true; } if ($updated) { $record->save(); } } private function storeReadingPhoto($upload, StabileServizioLettura $record, string $prefix): ?array { if (! is_object($upload) || ! method_exists($upload, 'storeAs')) { return null; } $ext = strtolower((string) ($upload->getClientOriginalExtension() ?: 'jpg')); $unit = (int) ($record->unita_immobiliare_id ?? 0); $filename = implode('-', array_filter([ 'stabile' . (int) $record->stabile_id, 'servizio' . (int) $record->stabile_servizio_id, $unit > 0 ? 'ui' . $unit : null, $prefix, now()->format('YmdHis'), ])) . '.' . $ext; $path = $upload->storeAs('letture-servizi/foto', $filename, 'public'); return [ 'path' => $path, 'original_name' => (string) $upload->getClientOriginalName(), 'metadata' => $this->extractImageMetadata($path), ]; } private function extractImageMetadata(string $path): array { $absolutePath = \Illuminate\Support\Facades\Storage::disk('public')->path($path); $metadata = [ 'path' => $path, 'size' => is_file($absolutePath) ? filesize($absolutePath) : null, ]; $imageSize = @getimagesize($absolutePath); if (is_array($imageSize)) { $metadata['width'] = $imageSize[0] ?? null; $metadata['height'] = $imageSize[1] ?? null; $metadata['mime'] = $imageSize['mime'] ?? null; } if (function_exists('exif_read_data')) { try { $exif = @exif_read_data($absolutePath); if (is_array($exif)) { $metadata['exif_datetime'] = $exif['DateTimeOriginal'] ?? ($exif['DateTime'] ?? null); $metadata['gps'] = [ 'lat' => $exif['GPSLatitude'] ?? null, 'lat_ref' => $exif['GPSLatitudeRef'] ?? null, 'lng' => $exif['GPSLongitude'] ?? null, 'lng_ref' => $exif['GPSLongitudeRef'] ?? null, ]; $metadata['device'] = trim(implode(' ', array_filter([ $exif['Make'] ?? null, $exif['Model'] ?? null, ]))); } } catch (\Throwable) { } } return $metadata; } private function resolveReadingAssignmentLabel(StabileServizioLettura $record): string { $ui = $record->unitaImmobiliare; if ($ui instanceof UnitaImmobiliare) { $cod = trim((string) ($ui->codice_unita ?? '')); $den = trim((string) ($ui->denominazione ?? '')); return trim(($cod !== '' ? $cod : ('UI #' . $ui->id)) . ($den !== '' ? (' - ' . $den) : '')); } $commonAsset = trim((string) data_get($record->servizio?->meta, 'common_asset_label', '')); $servedArea = trim((string) data_get($record->servizio?->meta, 'served_area_label', '')); $counterType = trim((string) data_get($record->servizio?->meta, 'counter_scope', '')); $parts = array_filter([ $commonAsset, $servedArea, $counterType !== '' ? 'Contatore ' . $counterType : null, ]); return $parts !== [] ? implode(' - ', $parts) : 'Contatore generale / bene comune'; } /** * @return array */ private function getReadingPhotoEntries(StabileServizioLettura $record): array { $entries = []; $append = function (string $path, ?string $name = null, $lat = null, $lng = null) use (&$entries): void { $path = trim($path); if ($path === '' || isset($entries[$path]) || ! Storage::disk('public')->exists($path)) { return; } $entries[$path] = [ 'name' => trim((string) ($name ?? '')) !== '' ? trim((string) $name) : basename($path), 'path' => $path, 'url' => Storage::disk('public')->url($path), 'latitude' => is_numeric($lat) ? (float) $lat : null, 'longitude' => is_numeric($lng) ? (float) $lng : null, ]; }; if (filled($record->lettura_foto_path)) { $append( (string) $record->lettura_foto_path, (string) ($record->lettura_foto_original_name ?? ''), data_get($record->lettura_foto_metadata, 'gps_decimal.lat'), data_get($record->lettura_foto_metadata, 'gps_decimal.lng') ); } $photos = is_array(data_get($record->lettura_foto_metadata, 'photos')) ? data_get($record->lettura_foto_metadata, 'photos') : []; foreach ($photos as $photo) { $append( (string) ($photo['path'] ?? ''), (string) ($photo['original_name'] ?? $photo['stored_basename'] ?? ''), data_get($photo, 'metadata.gps_decimal.lat'), data_get($photo, 'metadata.gps_decimal.lng') ); } return array_values($entries); } private function resolveReadingPrimaryPhotoPath(StabileServizioLettura $record): ?string { $photos = $this->getReadingPhotoEntries($record); return $photos !== [] ? (string) ($photos[0]['path'] ?? '') : null; } private function resolveReadingPrimaryPhotoUrl(StabileServizioLettura $record): ?string { $path = $this->resolveReadingPrimaryPhotoPath($record); if ($path === null || ! Storage::disk('public')->exists($path)) { return null; } return Storage::disk('public')->url($path); } private function resolveReadingMapUrl(StabileServizioLettura $record): ?string { $lat = data_get($record->lettura_foto_metadata, 'gps_decimal.lat'); $lng = data_get($record->lettura_foto_metadata, 'gps_decimal.lng'); if (is_numeric($lat) && is_numeric($lng)) { return 'https://maps.google.com/?q=' . $lat . ',' . $lng; } $photos = is_array(data_get($record->lettura_foto_metadata, 'photos')) ? data_get($record->lettura_foto_metadata, 'photos') : []; foreach ($photos as $photo) { $gpsLat = data_get($photo, 'metadata.gps_decimal.lat'); $gpsLng = data_get($photo, 'metadata.gps_decimal.lng'); if (is_numeric($gpsLat) && is_numeric($gpsLng)) { return 'https://maps.google.com/?q=' . $gpsLat . ',' . $gpsLng; } } return null; } }