netgescon-day0/app/Filament/Pages/Condomini/LettureServiziArchivio.php

1271 lines
60 KiB
PHP

<?php
namespace App\Filament\Pages\Condomini;
use App\Filament\Pages\Contabilita\FatturaElettronicaScheda;
use App\Models\Fornitore;
use App\Models\Stabile;
use App\Models\StabileServizio;
use App\Models\StabileServizioLettura;
use App\Models\UnitaImmobiliare;
use App\Models\User;
use App\Models\VoceSpesa;
use App\Support\AnnoGestioneContext;
use App\Support\StabileContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\FileUpload;
use Filament\Forms\Components\Select;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Filters\SelectFilter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Storage;
use UnitEnum;
class LettureServiziArchivio extends Page implements HasTable
{
use InteractsWithTable;
public ?int $servizioFilter = null;
public string $archivioScope = 'active';
protected static ?string $navigationLabel = 'Letture servizi / contatori';
protected static ?string $title = 'Letture servizi / contatori';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
protected static UnitEnum|string|null $navigationGroup = 'Stabile';
protected static ?int $navigationSort = 34;
protected static ?string $slug = 'condomini/letture-servizi';
protected string $view = 'filament.pages.gescon.table';
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->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,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([
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('Attribuzione')
->formatStateUsing(function ($state, StabileServizioLettura $record): string {
return $this->resolveReadingAssignmentLabel($record);
})
->wrap()
->toggleable(),
TextColumn::make('rilevatore_nome')
->label('Letturista / nominativo')
->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—')
->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('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',
};
})
->wrap()
->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' : '—';
})
->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) : ''));
})
->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('apriFoto')
->label('Foto')
->icon('heroicon-o-photo')
->url(fn(StabileServizioLettura $record): ?string => $this->resolveReadingPrimaryPhotoUrl($record))
->openUrlInNewTab()
->visible(fn(StabileServizioLettura $record): bool => $this->resolveReadingPrimaryPhotoUrl($record) !== null),
Action::make('mappaFoto')
->label('Mappa')
->icon('heroicon-o-map')
->url(fn(StabileServizioLettura $record): ?string => $this->resolveReadingMapUrl($record))
->openUrlInNewTab()
->visible(fn(StabileServizioLettura $record): bool => $this->resolveReadingMapUrl($record) !== null),
Action::make('riattribuisci')
->label('Riattribuisci')
->icon('heroicon-o-user-circle')
->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('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<int, string>
*/
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<int, string>
*/
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<int, string>
*/
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<int, string>
*/
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<int, string>
*/
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<string, string>
*/
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<int, string>
*/
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
{
$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';
}
private function resolveReadingPrimaryPhotoPath(StabileServizioLettura $record): ?string
{
if (filled($record->lettura_foto_path)) {
return (string) $record->lettura_foto_path;
}
$photos = is_array(data_get($record->lettura_foto_metadata, 'photos')) ? data_get($record->lettura_foto_metadata, 'photos') : [];
foreach ($photos as $photo) {
$path = trim((string) ($photo['path'] ?? ''));
if ($path !== '') {
return $path;
}
}
return 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;
}
}