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'; $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', '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', '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([ TextColumn::make('periodo_dal')->label('Dal')->date('d/m/Y')->sortable()->toggleable(), TextColumn::make('periodo_al')->label('Al')->date('d/m/Y')->sortable(), 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); }) ->wrap() ->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) : '—'; }) ->wrap() ->toggleable(), TextColumn::make('servizio.contatore_matricola')->label('Matricola')->toggleable(isToggledHiddenByDefault: true), TextColumn::make('unitaImmobiliare.denominazione') ->label('Unità immobiliare') ->formatStateUsing(function ($state, StabileServizioLettura $record): string { $ui = $record->unitaImmobiliare; if (! $ui) { return '—'; } $cod = trim((string) ($ui->codice_unita ?? '')); $den = trim((string) ($ui->denominazione ?? '')); if ($cod !== '' && $den !== '') { return $cod . ' - ' . $den; } return $cod !== '' ? $cod : ($den !== '' ? $den : ('UI #' . $ui->id)); }) ->wrap() ->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(), TextColumn::make('importo_totale')->label('Importo')->money('EUR')->sortable()->toggleable(), TextColumn::make('tipologia_lettura') ->label('Tipo lettura') ->formatStateUsing(fn($state) => $tipologiaOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—')) ->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) : '—', }; }) ->toggleable(), TextColumn::make('workflow_stato') ->label('Workflow') ->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? (string) $state : 'Acquisita') ->badge() ->toggleable(isToggledHiddenByDefault: true), 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) : '')); }) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('prossima_lettura_scadenza_at') ->label('Scadenza prossima lettura') ->dateTime('d/m/Y H:i') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('deadline_lettura_at') ->label('Deadline campagna') ->dateTime('d/m/Y H:i') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('sollecito_inviato_at') ->label('Sollecito') ->dateTime('d/m/Y H:i') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('rilevatore_tipo') ->label('Rilevatore') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('lettura_precedente_valore') ->label('Lettura precedente') ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('riferimento_acquisizione')->label('Riferimento')->toggleable(isToggledHiddenByDefault: true), TextColumn::make('archivio_documentale_path') ->label('Archivio documentale') ->wrap() ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('fatturaElettronica.numero_fattura') ->label('Fattura FE') ->url(fn(StabileServizioLettura $record): ?string => $record->fattura_elettronica_id ? FatturaElettronicaScheda::getUrl(['id' => (int) $record->fattura_elettronica_id], panel : 'admin-filament') : null) ->openUrlInNewTab() ->toggleable(), TextColumn::make('voceSpesa.descrizione')->label('Voce spesa')->wrap()->toggleable(isToggledHiddenByDefault: true), TextColumn::make('voceSpesa.tipo_gestione') ->label('Gestione') ->formatStateUsing(fn($state) => $gestioneOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—')) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('fornitore.ragione_sociale')->label('Fornitore')->wrap()->toggleable(isToggledHiddenByDefault: true), TextColumn::make('created_at')->label('Creato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true), ]) ->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('edit') ->label('Modifica') ->icon('heroicon-o-pencil-square') ->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') ->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']) ->mapWithKeys(function (StabileServizio $s): array { $tipo = strtoupper(trim((string) ($s->tipo ?? ''))); $nome = trim((string) ($s->nome ?? '')); $matr = trim((string) ($s->contatore_matricola ?? '')); $label = $nome !== '' ? $nome : ($tipo !== '' ? $tipo : ('Servizio #' . $s->id)); 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 []; } return UnitaImmobiliare::query() ->where('stabile_id', (int) $stabileId) ->orderBy('codice_unita') ->orderBy('interno') ->orderBy('id') ->get(['id', 'codice_unita', 'denominazione', 'interno']) ->mapWithKeys(function (UnitaImmobiliare $u): array { $cod = trim((string) ($u->codice_unita ?? '')); $den = trim((string) ($u->denominazione ?? '')); $int = trim((string) ($u->interno ?? '')); $label = $cod !== '' ? $cod : ('UI #' . $u->id); if ($den !== '') { $label .= ' - ' . $den; } if ($int !== '') { $label .= ' (Int. ' . $int . ')'; } return [(int) $u->id => $label]; }) ->all(); } /** * @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(); } 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 { if ($record->unita_immobiliare_id === null) { return; } $previous = StabileServizioLettura::query() ->where('stabile_servizio_id', (int) $record->stabile_servizio_id) ->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; } }