1397 lines
68 KiB
PHP
1397 lines
68 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';
|
|
|
|
$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([
|
|
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('spostaContatoreComune')
|
|
->label('Sposta a contatore comune')
|
|
->icon('heroicon-o-arrow-path-rounded-square')
|
|
->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')
|
|
->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();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
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';
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|