Finalize water workflows and shared asset management
This commit is contained in:
parent
d95fba5740
commit
7dd54f3a6f
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
class LettureServiziArchivio extends Page implements HasTable
|
class LettureServiziArchivio extends Page implements HasTable
|
||||||
|
|
@ -37,9 +39,9 @@ class LettureServiziArchivio extends Page implements HasTable
|
||||||
public ?int $servizioFilter = null;
|
public ?int $servizioFilter = null;
|
||||||
public string $archivioScope = 'active';
|
public string $archivioScope = 'active';
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Letture servizi';
|
protected static ?string $navigationLabel = 'Letture servizi / contatori';
|
||||||
|
|
||||||
protected static ?string $title = 'Letture servizi';
|
protected static ?string $title = 'Letture servizi / contatori';
|
||||||
|
|
||||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-clipboard-document-list';
|
||||||
|
|
||||||
|
|
@ -127,7 +129,7 @@ protected function getTableQuery(): Builder
|
||||||
->when($this->servizioFilter, fn(Builder $q) => $q->where('stabile_servizio_id', (int) $this->servizioFilter))
|
->when($this->servizioFilter, fn(Builder $q) => $q->where('stabile_servizio_id', (int) $this->servizioFilter))
|
||||||
->with([
|
->with([
|
||||||
'stabile:id,codice_stabile,denominazione',
|
'stabile:id,codice_stabile,denominazione',
|
||||||
'servizio:id,stabile_id,tipo,nome,contatore_matricola',
|
'servizio:id,stabile_id,tipo,nome,contatore_matricola,meta',
|
||||||
'fornitore:id,ragione_sociale',
|
'fornitore:id,ragione_sociale',
|
||||||
'voceSpesa:id,descrizione,tipo_gestione',
|
'voceSpesa:id,descrizione,tipo_gestione',
|
||||||
'unitaImmobiliare:id,codice_unita,denominazione,interno,scala,piano',
|
'unitaImmobiliare:id,codice_unita,denominazione,interno,scala,piano',
|
||||||
|
|
@ -168,6 +170,11 @@ public function table(Table $table): Table
|
||||||
'assicurazione' => 'Assicurazione',
|
'assicurazione' => 'Assicurazione',
|
||||||
'privacy' => 'Privacy',
|
'privacy' => 'Privacy',
|
||||||
'antenna' => 'Antenna',
|
'antenna' => 'Antenna',
|
||||||
|
'antincendio' => 'Antincendio',
|
||||||
|
'citofonia' => 'Citofonia',
|
||||||
|
'locale_comune' => 'Locale comune',
|
||||||
|
'spazio_comune' => 'Spazio / area comune',
|
||||||
|
'impianto' => 'Impianto comune',
|
||||||
'altro' => 'Altro',
|
'altro' => 'Altro',
|
||||||
])
|
])
|
||||||
->query(function (Builder $query, array $data): Builder {
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
|
@ -241,24 +248,19 @@ public function table(Table $table): Table
|
||||||
TextColumn::make('servizio.contatore_matricola')->label('Matricola')->toggleable(isToggledHiddenByDefault: true),
|
TextColumn::make('servizio.contatore_matricola')->label('Matricola')->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
TextColumn::make('unitaImmobiliare.denominazione')
|
TextColumn::make('unitaImmobiliare.denominazione')
|
||||||
->label('Unità immobiliare')
|
->label('Attribuzione')
|
||||||
->formatStateUsing(function ($state, StabileServizioLettura $record): string {
|
->formatStateUsing(function ($state, StabileServizioLettura $record): string {
|
||||||
$ui = $record->unitaImmobiliare;
|
return $this->resolveReadingAssignmentLabel($record);
|
||||||
if (! $ui) {
|
|
||||||
return '—';
|
|
||||||
}
|
|
||||||
|
|
||||||
$cod = trim((string) ($ui->codice_unita ?? ''));
|
|
||||||
$den = trim((string) ($ui->denominazione ?? ''));
|
|
||||||
if ($cod !== '' && $den !== '') {
|
|
||||||
return $cod . ' - ' . $den;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cod !== '' ? $cod : ($den !== '' ? $den : ('UI #' . $ui->id));
|
|
||||||
})
|
})
|
||||||
->wrap()
|
->wrap()
|
||||||
->toggleable(),
|
->toggleable(),
|
||||||
|
|
||||||
|
TextColumn::make('rilevatore_nome')
|
||||||
|
->label('Letturista / nominativo')
|
||||||
|
->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—')
|
||||||
|
->wrap()
|
||||||
|
->toggleable(),
|
||||||
|
|
||||||
TextColumn::make('consumo_valore')
|
TextColumn::make('consumo_valore')
|
||||||
->label('Consumo')
|
->label('Consumo')
|
||||||
->formatStateUsing(function ($state, StabileServizioLettura $record): string {
|
->formatStateUsing(function ($state, StabileServizioLettura $record): string {
|
||||||
|
|
@ -301,6 +303,35 @@ public function table(Table $table): Table
|
||||||
->badge()
|
->badge()
|
||||||
->toggleable(isToggledHiddenByDefault: true),
|
->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')
|
TextColumn::make('protocollo_numero')
|
||||||
->label('Protocollo')
|
->label('Protocollo')
|
||||||
->formatStateUsing(function ($state, StabileServizioLettura $record): string {
|
->formatStateUsing(function ($state, StabileServizioLettura $record): string {
|
||||||
|
|
@ -528,6 +559,78 @@ public function table(Table $table): Table
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
->actions([
|
->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')
|
Action::make('edit')
|
||||||
->label('Modifica')
|
->label('Modifica')
|
||||||
->icon('heroicon-o-pencil-square')
|
->icon('heroicon-o-pencil-square')
|
||||||
|
|
@ -675,13 +778,17 @@ private function getServiziOptions(): array
|
||||||
->where('stabile_id', (int) $stabileId)
|
->where('stabile_id', (int) $stabileId)
|
||||||
->orderBy('tipo')
|
->orderBy('tipo')
|
||||||
->orderBy('nome')
|
->orderBy('nome')
|
||||||
->get(['id', 'tipo', 'nome', 'contatore_matricola'])
|
->get(['id', 'tipo', 'nome', 'contatore_matricola', 'meta'])
|
||||||
->mapWithKeys(function (StabileServizio $s): array {
|
->mapWithKeys(function (StabileServizio $s): array {
|
||||||
$tipo = strtoupper(trim((string) ($s->tipo ?? '')));
|
$tipo = strtoupper(trim((string) ($s->tipo ?? '')));
|
||||||
$nome = trim((string) ($s->nome ?? ''));
|
$nome = trim((string) ($s->nome ?? ''));
|
||||||
$matr = trim((string) ($s->contatore_matricola ?? ''));
|
$matr = trim((string) ($s->contatore_matricola ?? ''));
|
||||||
|
$asset = trim((string) data_get($s->meta, 'common_asset_label', ''));
|
||||||
|
|
||||||
$label = $nome !== '' ? $nome : ($tipo !== '' ? $tipo : ('Servizio #' . $s->id));
|
$label = $nome !== '' ? $nome : ($tipo !== '' ? $tipo : ('Servizio #' . $s->id));
|
||||||
|
if ($asset !== '' && ! str_contains(mb_strtolower($label), mb_strtolower($asset))) {
|
||||||
|
$label .= ' - ' . $asset;
|
||||||
|
}
|
||||||
if ($matr !== '') {
|
if ($matr !== '') {
|
||||||
$label .= ' (' . $matr . ')';
|
$label .= ' (' . $matr . ')';
|
||||||
}
|
}
|
||||||
|
|
@ -782,16 +889,36 @@ private function getUnitaOptions(): array
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnitaImmobiliare::query()
|
$units = UnitaImmobiliare::query()
|
||||||
->where('stabile_id', (int) $stabileId)
|
->where('stabile_id', (int) $stabileId)
|
||||||
->orderBy('codice_unita')
|
->orderBy('codice_unita')
|
||||||
->orderBy('interno')
|
->orderBy('interno')
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->get(['id', 'codice_unita', 'denominazione', 'interno'])
|
->get(['id', 'codice_unita', 'denominazione', 'interno'])
|
||||||
->mapWithKeys(function (UnitaImmobiliare $u): array {
|
->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 ?? ''));
|
$cod = trim((string) ($u->codice_unita ?? ''));
|
||||||
$den = trim((string) ($u->denominazione ?? ''));
|
$den = trim((string) ($u->denominazione ?? ''));
|
||||||
$int = trim((string) ($u->interno ?? ''));
|
$int = trim((string) ($u->interno ?? ''));
|
||||||
|
$nom = trim((string) ($latestNominativi[(int) $u->id] ?? ''));
|
||||||
|
|
||||||
$label = $cod !== '' ? $cod : ('UI #' . $u->id);
|
$label = $cod !== '' ? $cod : ('UI #' . $u->id);
|
||||||
if ($den !== '') {
|
if ($den !== '') {
|
||||||
|
|
@ -800,10 +927,33 @@ private function getUnitaOptions(): array
|
||||||
if ($int !== '') {
|
if ($int !== '') {
|
||||||
$label .= ' (Int. ' . $int . ')';
|
$label .= ' (Int. ' . $int . ')';
|
||||||
}
|
}
|
||||||
|
if ($nom !== '') {
|
||||||
|
$label .= ' · ' . $nom;
|
||||||
|
}
|
||||||
|
|
||||||
return [(int) $u->id => $label];
|
return [(int) $u->id => $label];
|
||||||
})
|
})
|
||||||
->all();
|
->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',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -921,13 +1071,13 @@ private function markOverdueWaterReminders(): int
|
||||||
|
|
||||||
private function hydrateReadingWithPreviousData(StabileServizioLettura $record): void
|
private function hydrateReadingWithPreviousData(StabileServizioLettura $record): void
|
||||||
{
|
{
|
||||||
if ($record->unita_immobiliare_id === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$previous = StabileServizioLettura::query()
|
$previous = StabileServizioLettura::query()
|
||||||
->where('stabile_servizio_id', (int) $record->stabile_servizio_id)
|
->where('stabile_servizio_id', (int) $record->stabile_servizio_id)
|
||||||
->where('unita_immobiliare_id', (int) $record->unita_immobiliare_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)
|
->where('id', '!=', (int) $record->id)
|
||||||
->latest('periodo_al')
|
->latest('periodo_al')
|
||||||
->latest('id')
|
->latest('id')
|
||||||
|
|
@ -1045,4 +1195,76 @@ private function extractImageMetadata(string $path): array
|
||||||
|
|
||||||
return $metadata;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
use BackedEnum;
|
use BackedEnum;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Forms\Components\Select;
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\Toggle;
|
use Filament\Forms\Components\Toggle;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
|
|
@ -35,9 +36,9 @@ class ServiziStabileArchivio extends Page implements HasTable
|
||||||
{
|
{
|
||||||
use InteractsWithTable;
|
use InteractsWithTable;
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Servizi / Utenze';
|
protected static ?string $navigationLabel = 'Servizi / Beni comuni';
|
||||||
|
|
||||||
protected static ?string $title = 'Servizi / Utenze';
|
protected static ?string $title = 'Servizi / Beni comuni';
|
||||||
|
|
||||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-rectangle-stack';
|
||||||
|
|
||||||
|
|
@ -268,18 +269,11 @@ protected function getTableQuery(): Builder
|
||||||
|
|
||||||
public function table(Table $table): Table
|
public function table(Table $table): Table
|
||||||
{
|
{
|
||||||
$tipoOptions = [
|
$tipoOptions = $this->getServiceTypeOptions();
|
||||||
'acqua' => 'Acqua',
|
$commonScopeOptions = $this->getCommonAssetScopeOptions();
|
||||||
'energia' => 'Energia',
|
$allocationOptions = $this->getAllocationScopeOptions();
|
||||||
'gas' => 'Gas',
|
$fiscalScopeOptions = $this->getFiscalScopeOptions();
|
||||||
'riscaldamento' => 'Riscaldamento',
|
$counterScopeOptions = $this->getCounterScopeOptions();
|
||||||
'ascensore' => 'Ascensore',
|
|
||||||
'pulizie' => 'Pulizie',
|
|
||||||
'assicurazione' => 'Assicurazione',
|
|
||||||
'privacy' => 'Privacy',
|
|
||||||
'antenna' => 'Antenna',
|
|
||||||
'altro' => 'Altro',
|
|
||||||
];
|
|
||||||
|
|
||||||
$gestioneOptions = [
|
$gestioneOptions = [
|
||||||
'ordinaria' => 'Ordinaria',
|
'ordinaria' => 'Ordinaria',
|
||||||
|
|
@ -294,6 +288,30 @@ public function table(Table $table): Table
|
||||||
->label('Tipo servizio')
|
->label('Tipo servizio')
|
||||||
->options($tipoOptions),
|
->options($tipoOptions),
|
||||||
|
|
||||||
|
SelectFilter::make('common_asset_scope')
|
||||||
|
->label('Ambito bene comune')
|
||||||
|
->options($commonScopeOptions)
|
||||||
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
$value = trim((string) ($data['value'] ?? ''));
|
||||||
|
if ($value === '') {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->where('meta->common_asset_scope', $value);
|
||||||
|
}),
|
||||||
|
|
||||||
|
SelectFilter::make('counter_scope')
|
||||||
|
->label('Tipo contatore')
|
||||||
|
->options($counterScopeOptions)
|
||||||
|
->query(function (Builder $query, array $data): Builder {
|
||||||
|
$value = trim((string) ($data['value'] ?? ''));
|
||||||
|
if ($value === '') {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->where('meta->counter_scope', $value);
|
||||||
|
}),
|
||||||
|
|
||||||
SelectFilter::make('tipo_gestione')
|
SelectFilter::make('tipo_gestione')
|
||||||
->label('Gestione (O/R/S)')
|
->label('Gestione (O/R/S)')
|
||||||
->options($gestioneOptions)
|
->options($gestioneOptions)
|
||||||
|
|
@ -309,11 +327,35 @@ public function table(Table $table): Table
|
||||||
->columns([
|
->columns([
|
||||||
TextColumn::make('tipo')
|
TextColumn::make('tipo')
|
||||||
->label('Tipo')
|
->label('Tipo')
|
||||||
->formatStateUsing(fn($state) => $tipoOptions[strtolower(trim((string) $state))] ?? (string) ($state ?? '—'))
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($tipoOptions, $state))
|
||||||
->sortable(),
|
->sortable(),
|
||||||
|
|
||||||
TextColumn::make('nome')->label('Nome')->searchable()->wrap()->sortable(),
|
TextColumn::make('nome')->label('Nome')->searchable()->wrap()->sortable(),
|
||||||
|
|
||||||
|
TextColumn::make('meta.common_asset_label')
|
||||||
|
->label('Bene / locale')
|
||||||
|
->formatStateUsing(fn($state): string => trim((string) $state) !== '' ? trim((string) $state) : '—')
|
||||||
|
->searchable()
|
||||||
|
->wrap()
|
||||||
|
->toggleable(),
|
||||||
|
|
||||||
|
TextColumn::make('meta.common_asset_scope')
|
||||||
|
->label('Ambito')
|
||||||
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($commonScopeOptions, $state))
|
||||||
|
->toggleable(),
|
||||||
|
|
||||||
|
TextColumn::make('meta.allocation_scope')
|
||||||
|
->label('Addebito')
|
||||||
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($allocationOptions, $state))
|
||||||
|
->wrap()
|
||||||
|
->toggleable(),
|
||||||
|
|
||||||
|
TextColumn::make('meta.fiscal_scope')
|
||||||
|
->label('Trattamento')
|
||||||
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($fiscalScopeOptions, $state))
|
||||||
|
->wrap()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
TextColumn::make('fornitore.ragione_sociale')->label('Fornitore')->searchable()->wrap()->toggleable(),
|
TextColumn::make('fornitore.ragione_sociale')->label('Fornitore')->searchable()->wrap()->toggleable(),
|
||||||
|
|
||||||
TextColumn::make('voceSpesa.descrizione')->label('Voce spesa')->searchable()->wrap()->toggleable(),
|
TextColumn::make('voceSpesa.descrizione')->label('Voce spesa')->searchable()->wrap()->toggleable(),
|
||||||
|
|
@ -350,6 +392,21 @@ public function table(Table $table): Table
|
||||||
TextColumn::make('codice_contratto')->label('Contratto')->searchable()->toggleable(isToggledHiddenByDefault: true),
|
TextColumn::make('codice_contratto')->label('Contratto')->searchable()->toggleable(isToggledHiddenByDefault: true),
|
||||||
TextColumn::make('contatore_matricola')->label('Matricola')->searchable()->toggleable(),
|
TextColumn::make('contatore_matricola')->label('Matricola')->searchable()->toggleable(),
|
||||||
|
|
||||||
|
TextColumn::make('meta.counter_scope')
|
||||||
|
->label('Contatore')
|
||||||
|
->formatStateUsing(fn($state) => $this->formatMetaOptionLabel($counterScopeOptions, $state))
|
||||||
|
->toggleable(),
|
||||||
|
|
||||||
|
IconColumn::make('meta.has_dedicated_meter')
|
||||||
|
->label('Ded.')
|
||||||
|
->boolean()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
|
TextColumn::make('meta.served_area_label')
|
||||||
|
->label('Area servita')
|
||||||
|
->wrap()
|
||||||
|
->toggleable(isToggledHiddenByDefault: true),
|
||||||
|
|
||||||
IconColumn::make('attivo')->label('Attivo')->boolean()->sortable(),
|
IconColumn::make('attivo')->label('Attivo')->boolean()->sortable(),
|
||||||
TextColumn::make('updated_at')->label('Aggiornato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true),
|
TextColumn::make('updated_at')->label('Aggiornato')->dateTime('d/m/Y H:i')->toggleable(isToggledHiddenByDefault: true),
|
||||||
])
|
])
|
||||||
|
|
@ -363,6 +420,16 @@ public function table(Table $table): Table
|
||||||
->options($tipoOptions)
|
->options($tipoOptions)
|
||||||
->required(),
|
->required(),
|
||||||
TextInput::make('nome')->label('Nome')->maxLength(255),
|
TextInput::make('nome')->label('Nome')->maxLength(255),
|
||||||
|
TextInput::make('common_asset_label')->label('Bene comune / locale / servizio')->maxLength(255),
|
||||||
|
Select::make('common_asset_scope')
|
||||||
|
->label('Ambito bene comune')
|
||||||
|
->options($commonScopeOptions),
|
||||||
|
Select::make('allocation_scope')
|
||||||
|
->label('Ripartizione / addebito')
|
||||||
|
->options($allocationOptions),
|
||||||
|
Select::make('fiscal_scope')
|
||||||
|
->label('Trattamento economico / fiscale')
|
||||||
|
->options($fiscalScopeOptions),
|
||||||
Select::make('fornitore_id')
|
Select::make('fornitore_id')
|
||||||
->label('Fornitore')
|
->label('Fornitore')
|
||||||
->searchable()
|
->searchable()
|
||||||
|
|
@ -375,6 +442,12 @@ public function table(Table $table): Table
|
||||||
TextInput::make('codice_cliente')->label('Codice cliente')->maxLength(255),
|
TextInput::make('codice_cliente')->label('Codice cliente')->maxLength(255),
|
||||||
TextInput::make('codice_contratto')->label('Codice contratto')->maxLength(255),
|
TextInput::make('codice_contratto')->label('Codice contratto')->maxLength(255),
|
||||||
TextInput::make('contatore_matricola')->label('Matricola contatore')->maxLength(255),
|
TextInput::make('contatore_matricola')->label('Matricola contatore')->maxLength(255),
|
||||||
|
Toggle::make('has_dedicated_meter')->label('Contatore dedicato')->default(false),
|
||||||
|
Select::make('counter_scope')
|
||||||
|
->label('Tipo contatore')
|
||||||
|
->options($counterScopeOptions)
|
||||||
|
->default('assente'),
|
||||||
|
TextInput::make('served_area_label')->label('Area servita / utilizzo')->maxLength(255),
|
||||||
Select::make('canale_acquisizione')
|
Select::make('canale_acquisizione')
|
||||||
->label('Canale acquisizione letture')
|
->label('Canale acquisizione letture')
|
||||||
->options([
|
->options([
|
||||||
|
|
@ -389,6 +462,7 @@ public function table(Table $table): Table
|
||||||
TextInput::make('costo_lettura_unitario')->label('Costo lettura (unitario)')->numeric(),
|
TextInput::make('costo_lettura_unitario')->label('Costo lettura (unitario)')->numeric(),
|
||||||
TextInput::make('costo_ripartizione')->label('Costo ripartizione')->numeric(),
|
TextInput::make('costo_ripartizione')->label('Costo ripartizione')->numeric(),
|
||||||
Toggle::make('ripartizione_esterna')->label('Ripartizione affidata a ditta esterna')->default(false),
|
Toggle::make('ripartizione_esterna')->label('Ripartizione affidata a ditta esterna')->default(false),
|
||||||
|
Textarea::make('operative_notes')->label('Note operative')->rows(3)->maxLength(2000),
|
||||||
Toggle::make('attivo')->label('Attivo')->default(true),
|
Toggle::make('attivo')->label('Attivo')->default(true),
|
||||||
])
|
])
|
||||||
->action(function (array $data): void {
|
->action(function (array $data): void {
|
||||||
|
|
@ -405,7 +479,7 @@ public function table(Table $table): Table
|
||||||
StabileServizio::query()->create([
|
StabileServizio::query()->create([
|
||||||
'stabile_id' => (int) $stabileId,
|
'stabile_id' => (int) $stabileId,
|
||||||
'tipo' => strtolower(trim((string) ($data['tipo'] ?? ''))),
|
'tipo' => strtolower(trim((string) ($data['tipo'] ?? ''))),
|
||||||
'nome' => trim((string) ($data['nome'] ?? '')),
|
'nome' => $this->resolveServiceDisplayName($data),
|
||||||
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
|
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
|
||||||
'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null,
|
'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null,
|
||||||
'codice_utenza' => trim((string) ($data['codice_utenza'] ?? '')) ?: null,
|
'codice_utenza' => trim((string) ($data['codice_utenza'] ?? '')) ?: null,
|
||||||
|
|
@ -413,14 +487,7 @@ public function table(Table $table): Table
|
||||||
'codice_contratto' => trim((string) ($data['codice_contratto'] ?? '')) ?: null,
|
'codice_contratto' => trim((string) ($data['codice_contratto'] ?? '')) ?: null,
|
||||||
'contatore_matricola' => trim((string) ($data['contatore_matricola'] ?? '')) ?: null,
|
'contatore_matricola' => trim((string) ($data['contatore_matricola'] ?? '')) ?: null,
|
||||||
'attivo' => (bool) ($data['attivo'] ?? true),
|
'attivo' => (bool) ($data['attivo'] ?? true),
|
||||||
'meta' => [
|
'meta' => $this->buildServiceMetaPayload([], $data),
|
||||||
'canale_acquisizione' => trim((string) ($data['canale_acquisizione'] ?? '')) ?: null,
|
|
||||||
'email_raccolta_letture' => trim((string) ($data['email_raccolta_letture'] ?? '')) ?: null,
|
|
||||||
'whatsapp_raccolta_letture' => trim((string) ($data['whatsapp_raccolta_letture'] ?? '')) ?: null,
|
|
||||||
'costo_lettura_unitario' => is_numeric($data['costo_lettura_unitario'] ?? null) ? (float) $data['costo_lettura_unitario'] : null,
|
|
||||||
'costo_ripartizione' => is_numeric($data['costo_ripartizione'] ?? null) ? (float) $data['costo_ripartizione'] : null,
|
|
||||||
'ripartizione_esterna' => (bool) ($data['ripartizione_esterna'] ?? false),
|
|
||||||
],
|
|
||||||
]);
|
]);
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|
@ -444,6 +511,16 @@ public function table(Table $table): Table
|
||||||
->options($tipoOptions)
|
->options($tipoOptions)
|
||||||
->required(),
|
->required(),
|
||||||
TextInput::make('nome')->label('Nome')->maxLength(255),
|
TextInput::make('nome')->label('Nome')->maxLength(255),
|
||||||
|
TextInput::make('common_asset_label')->label('Bene comune / locale / servizio')->maxLength(255),
|
||||||
|
Select::make('common_asset_scope')
|
||||||
|
->label('Ambito bene comune')
|
||||||
|
->options($commonScopeOptions),
|
||||||
|
Select::make('allocation_scope')
|
||||||
|
->label('Ripartizione / addebito')
|
||||||
|
->options($allocationOptions),
|
||||||
|
Select::make('fiscal_scope')
|
||||||
|
->label('Trattamento economico / fiscale')
|
||||||
|
->options($fiscalScopeOptions),
|
||||||
Select::make('fornitore_id')
|
Select::make('fornitore_id')
|
||||||
->label('Fornitore')
|
->label('Fornitore')
|
||||||
->searchable()
|
->searchable()
|
||||||
|
|
@ -456,6 +533,11 @@ public function table(Table $table): Table
|
||||||
TextInput::make('codice_cliente')->label('Codice cliente')->maxLength(255),
|
TextInput::make('codice_cliente')->label('Codice cliente')->maxLength(255),
|
||||||
TextInput::make('codice_contratto')->label('Codice contratto')->maxLength(255),
|
TextInput::make('codice_contratto')->label('Codice contratto')->maxLength(255),
|
||||||
TextInput::make('contatore_matricola')->label('Matricola contatore')->maxLength(255),
|
TextInput::make('contatore_matricola')->label('Matricola contatore')->maxLength(255),
|
||||||
|
Toggle::make('has_dedicated_meter')->label('Contatore dedicato')->default(false),
|
||||||
|
Select::make('counter_scope')
|
||||||
|
->label('Tipo contatore')
|
||||||
|
->options($counterScopeOptions),
|
||||||
|
TextInput::make('served_area_label')->label('Area servita / utilizzo')->maxLength(255),
|
||||||
Select::make('canale_acquisizione')
|
Select::make('canale_acquisizione')
|
||||||
->label('Canale acquisizione letture')
|
->label('Canale acquisizione letture')
|
||||||
->options([
|
->options([
|
||||||
|
|
@ -470,37 +552,40 @@ public function table(Table $table): Table
|
||||||
TextInput::make('costo_lettura_unitario')->label('Costo lettura (unitario)')->numeric(),
|
TextInput::make('costo_lettura_unitario')->label('Costo lettura (unitario)')->numeric(),
|
||||||
TextInput::make('costo_ripartizione')->label('Costo ripartizione')->numeric(),
|
TextInput::make('costo_ripartizione')->label('Costo ripartizione')->numeric(),
|
||||||
Toggle::make('ripartizione_esterna')->label('Ripartizione affidata a ditta esterna')->default(false),
|
Toggle::make('ripartizione_esterna')->label('Ripartizione affidata a ditta esterna')->default(false),
|
||||||
|
Textarea::make('operative_notes')->label('Note operative')->rows(3)->maxLength(2000),
|
||||||
Toggle::make('attivo')->label('Attivo')->default(true),
|
Toggle::make('attivo')->label('Attivo')->default(true),
|
||||||
])
|
])
|
||||||
->fillForm(fn(StabileServizio $record): array=> [
|
->fillForm(fn(StabileServizio $record): array=> [
|
||||||
'tipo' => (string) ($record->tipo ?? ''),
|
'tipo' => (string) ($record->tipo ?? ''),
|
||||||
'nome' => (string) ($record->nome ?? ''),
|
'nome' => (string) ($record->nome ?? ''),
|
||||||
|
'common_asset_label' => (string) (($record->meta['common_asset_label'] ?? '') ?: ''),
|
||||||
|
'common_asset_scope' => (string) (($record->meta['common_asset_scope'] ?? '') ?: ''),
|
||||||
|
'allocation_scope' => (string) (($record->meta['allocation_scope'] ?? '') ?: ''),
|
||||||
|
'fiscal_scope' => (string) (($record->meta['fiscal_scope'] ?? '') ?: ''),
|
||||||
'fornitore_id' => $record->fornitore_id,
|
'fornitore_id' => $record->fornitore_id,
|
||||||
'voce_spesa_id' => $record->voce_spesa_id,
|
'voce_spesa_id' => $record->voce_spesa_id,
|
||||||
'codice_utenza' => (string) ($record->codice_utenza ?? ''),
|
'codice_utenza' => (string) ($record->codice_utenza ?? ''),
|
||||||
'codice_cliente' => (string) ($record->codice_cliente ?? ''),
|
'codice_cliente' => (string) ($record->codice_cliente ?? ''),
|
||||||
'codice_contratto' => (string) ($record->codice_contratto ?? ''),
|
'codice_contratto' => (string) ($record->codice_contratto ?? ''),
|
||||||
'contatore_matricola' => (string) ($record->contatore_matricola ?? ''),
|
'contatore_matricola' => (string) ($record->contatore_matricola ?? ''),
|
||||||
|
'has_dedicated_meter' => (bool) ($record->meta['has_dedicated_meter'] ?? false),
|
||||||
|
'counter_scope' => (string) (($record->meta['counter_scope'] ?? '') ?: ''),
|
||||||
|
'served_area_label' => (string) (($record->meta['served_area_label'] ?? '') ?: ''),
|
||||||
'canale_acquisizione' => (string) (($record->meta['canale_acquisizione'] ?? '') ?: ''),
|
'canale_acquisizione' => (string) (($record->meta['canale_acquisizione'] ?? '') ?: ''),
|
||||||
'email_raccolta_letture' => (string) (($record->meta['email_raccolta_letture'] ?? '') ?: ''),
|
'email_raccolta_letture' => (string) (($record->meta['email_raccolta_letture'] ?? '') ?: ''),
|
||||||
'whatsapp_raccolta_letture' => (string) (($record->meta['whatsapp_raccolta_letture'] ?? '') ?: ''),
|
'whatsapp_raccolta_letture' => (string) (($record->meta['whatsapp_raccolta_letture'] ?? '') ?: ''),
|
||||||
'costo_lettura_unitario' => $record->meta['costo_lettura_unitario'] ?? null,
|
'costo_lettura_unitario' => $record->meta['costo_lettura_unitario'] ?? null,
|
||||||
'costo_ripartizione' => $record->meta['costo_ripartizione'] ?? null,
|
'costo_ripartizione' => $record->meta['costo_ripartizione'] ?? null,
|
||||||
'ripartizione_esterna' => (bool) ($record->meta['ripartizione_esterna'] ?? false),
|
'ripartizione_esterna' => (bool) ($record->meta['ripartizione_esterna'] ?? false),
|
||||||
|
'operative_notes' => (string) (($record->meta['operative_notes'] ?? '') ?: ''),
|
||||||
'attivo' => (bool) ($record->attivo ?? true),
|
'attivo' => (bool) ($record->attivo ?? true),
|
||||||
])
|
])
|
||||||
->action(function (StabileServizio $record, array $data): void {
|
->action(function (StabileServizio $record, array $data): void {
|
||||||
$meta = is_array($record->meta ?? null) ? $record->meta : [];
|
$meta = $this->buildServiceMetaPayload(is_array($record->meta ?? null) ? $record->meta : [], $data);
|
||||||
$meta['canale_acquisizione'] = trim((string) ($data['canale_acquisizione'] ?? '')) ?: null;
|
|
||||||
$meta['email_raccolta_letture'] = trim((string) ($data['email_raccolta_letture'] ?? '')) ?: null;
|
|
||||||
$meta['whatsapp_raccolta_letture'] = trim((string) ($data['whatsapp_raccolta_letture'] ?? '')) ?: null;
|
|
||||||
$meta['costo_lettura_unitario'] = is_numeric($data['costo_lettura_unitario'] ?? null) ? (float) $data['costo_lettura_unitario'] : null;
|
|
||||||
$meta['costo_ripartizione'] = is_numeric($data['costo_ripartizione'] ?? null) ? (float) $data['costo_ripartizione'] : null;
|
|
||||||
$meta['ripartizione_esterna'] = (bool) ($data['ripartizione_esterna'] ?? false);
|
|
||||||
|
|
||||||
$record->fill([
|
$record->fill([
|
||||||
'tipo' => strtolower(trim((string) ($data['tipo'] ?? ''))),
|
'tipo' => strtolower(trim((string) ($data['tipo'] ?? ''))),
|
||||||
'nome' => trim((string) ($data['nome'] ?? '')),
|
'nome' => $this->resolveServiceDisplayName($data, (string) ($record->nome ?? '')),
|
||||||
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
|
'fornitore_id' => (int) ($data['fornitore_id'] ?? 0) ?: null,
|
||||||
'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null,
|
'voce_spesa_id' => (int) ($data['voce_spesa_id'] ?? 0) ?: null,
|
||||||
'codice_utenza' => trim((string) ($data['codice_utenza'] ?? '')) ?: null,
|
'codice_utenza' => trim((string) ($data['codice_utenza'] ?? '')) ?: null,
|
||||||
|
|
@ -522,6 +607,142 @@ public function table(Table $table): Table
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function getServiceTypeOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function getCommonAssetScopeOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'locale' => 'Locale',
|
||||||
|
'spazio' => 'Spazio comune',
|
||||||
|
'impianto' => 'Impianto',
|
||||||
|
'area_esterna' => 'Area esterna / giardino',
|
||||||
|
'copertura' => 'Terrazzo / copertura',
|
||||||
|
'accesso' => 'Portone / accesso',
|
||||||
|
'servizio' => 'Servizio comune',
|
||||||
|
'sicurezza' => 'Sicurezza / antincendio',
|
||||||
|
'amministrativo'=> 'Privacy / assicurazione / studio',
|
||||||
|
'altro' => 'Altro',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function getAllocationScopeOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tutti_millesimi' => 'Tutti per millesimi',
|
||||||
|
'condomini_utilizzatori' => 'Condomini utilizzatori',
|
||||||
|
'inquilini_utilizzatori' => 'Inquilini utilizzatori',
|
||||||
|
'condomini_inquilini_utilizzatori'=> 'Condomini + inquilini utilizzatori',
|
||||||
|
'rimborso_spese' => 'Rimborso spese uso bene comune',
|
||||||
|
'gestione_studio' => 'Gestione studio / amministrazione',
|
||||||
|
'altro' => 'Altro criterio',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function getFiscalScopeOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'spesa_condominiale' => 'Spesa condominiale',
|
||||||
|
'rimborso_non_fiscale' => 'Rimborso spese non fiscale',
|
||||||
|
'da_fatturare' => 'Da fatturare',
|
||||||
|
'polizza_o_servizio' => 'Polizza / servizio',
|
||||||
|
'non_applicabile' => 'Non applicabile',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
private function getCounterScopeOptions(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'generale' => 'Generale',
|
||||||
|
'particolare' => 'Particolare / dedicato',
|
||||||
|
'assente' => 'Senza contatore',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatMetaOptionLabel(array $options, mixed $state): string
|
||||||
|
{
|
||||||
|
$value = strtolower(trim((string) $state));
|
||||||
|
|
||||||
|
return $options[$value] ?? ($value !== '' ? (string) $state : '—');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $currentMeta
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildServiceMetaPayload(array $currentMeta, array $data): array
|
||||||
|
{
|
||||||
|
$meta = $currentMeta;
|
||||||
|
$meta['common_asset_label'] = trim((string) ($data['common_asset_label'] ?? '')) ?: null;
|
||||||
|
$meta['common_asset_scope'] = trim((string) ($data['common_asset_scope'] ?? '')) ?: null;
|
||||||
|
$meta['allocation_scope'] = trim((string) ($data['allocation_scope'] ?? '')) ?: null;
|
||||||
|
$meta['fiscal_scope'] = trim((string) ($data['fiscal_scope'] ?? '')) ?: null;
|
||||||
|
$meta['has_dedicated_meter'] = (bool) ($data['has_dedicated_meter'] ?? false);
|
||||||
|
$meta['counter_scope'] = trim((string) ($data['counter_scope'] ?? '')) ?: (($meta['has_dedicated_meter'] ?? false) ? 'particolare' : 'assente');
|
||||||
|
$meta['served_area_label'] = trim((string) ($data['served_area_label'] ?? '')) ?: null;
|
||||||
|
$meta['canale_acquisizione'] = trim((string) ($data['canale_acquisizione'] ?? '')) ?: null;
|
||||||
|
$meta['email_raccolta_letture'] = trim((string) ($data['email_raccolta_letture'] ?? '')) ?: null;
|
||||||
|
$meta['whatsapp_raccolta_letture'] = trim((string) ($data['whatsapp_raccolta_letture'] ?? '')) ?: null;
|
||||||
|
$meta['costo_lettura_unitario'] = is_numeric($data['costo_lettura_unitario'] ?? null) ? (float) $data['costo_lettura_unitario'] : null;
|
||||||
|
$meta['costo_ripartizione'] = is_numeric($data['costo_ripartizione'] ?? null) ? (float) $data['costo_ripartizione'] : null;
|
||||||
|
$meta['ripartizione_esterna'] = (bool) ($data['ripartizione_esterna'] ?? false);
|
||||||
|
$meta['operative_notes'] = trim((string) ($data['operative_notes'] ?? '')) ?: null;
|
||||||
|
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
*/
|
||||||
|
private function resolveServiceDisplayName(array $data, string $fallback = ''): string
|
||||||
|
{
|
||||||
|
$name = trim((string) ($data['nome'] ?? ''));
|
||||||
|
if ($name !== '') {
|
||||||
|
return $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
$commonLabel = trim((string) ($data['common_asset_label'] ?? ''));
|
||||||
|
if ($commonLabel !== '') {
|
||||||
|
return $commonLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($fallback);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, string>
|
* @return array<int, string>
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -294,6 +294,7 @@ public function registraLetturaAcqua(): void
|
||||||
$letturaFine = round((float) $validated['letturaAttuale'], 3);
|
$letturaFine = round((float) $validated['letturaAttuale'], 3);
|
||||||
$consumo = $letturaInizio !== null ? round($letturaFine - $letturaInizio, 3) : null;
|
$consumo = $letturaInizio !== null ? round($letturaFine - $letturaInizio, 3) : null;
|
||||||
$readerName = trim((string) ($validated['rilevatoreNome'] ?? ''));
|
$readerName = trim((string) ($validated['rilevatoreNome'] ?? ''));
|
||||||
|
$consistency = $this->buildReadingConsistencyCheck((int) $servizio->id, (int) $unita->id, $letturaFine, $letturaInizio);
|
||||||
$readingDate = filled($validated['dataLettura'] ?? null)
|
$readingDate = filled($validated['dataLettura'] ?? null)
|
||||||
? Carbon::parse((string) $validated['dataLettura'])
|
? Carbon::parse((string) $validated['dataLettura'])
|
||||||
: now();
|
: now();
|
||||||
|
|
@ -390,6 +391,7 @@ public function registraLetturaAcqua(): void
|
||||||
'photo_assets' => $photoAssets,
|
'photo_assets' => $photoAssets,
|
||||||
'drive_relative_directory' => $storageDirectory,
|
'drive_relative_directory' => $storageDirectory,
|
||||||
'reader_label' => $this->resolveUnitReaderLabel($unita),
|
'reader_label' => $this->resolveUnitReaderLabel($unita),
|
||||||
|
'consistency_check' => $consistency,
|
||||||
],
|
],
|
||||||
'created_by' => (int) $user->id,
|
'created_by' => (int) $user->id,
|
||||||
]);
|
]);
|
||||||
|
|
@ -407,6 +409,14 @@ public function registraLetturaAcqua(): void
|
||||||
->body('Lettura salvata per unità ' . ((string) ($unita->codice_unita ?: ('#' . $unita->id))) . ' con riferimento #' . $row->id . '.')
|
->body('Lettura salvata per unità ' . ((string) ($unita->codice_unita ?: ('#' . $unita->id))) . ' con riferimento #' . $row->id . '.')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
|
if (($consistency['status'] ?? 'ok') !== 'ok') {
|
||||||
|
Notification::make()
|
||||||
|
->title('Controllo coerenza lettura')
|
||||||
|
->body((string) ($consistency['summary'] ?? 'La lettura va verificata rispetto allo storico.'))
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadStabileOptions(): void
|
private function loadStabileOptions(): void
|
||||||
|
|
@ -764,6 +774,94 @@ private function resolveCurrentUnitOption(): ?array
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{status:string,summary:string,delta:?float,average:?float,rule:?string}
|
||||||
|
*/
|
||||||
|
private function buildReadingConsistencyCheck(int $servizioId, int $unitaId, float $letturaFine, ?float $letturaInizio): array
|
||||||
|
{
|
||||||
|
if ($letturaInizio === null) {
|
||||||
|
return [
|
||||||
|
'status' => 'ok',
|
||||||
|
'summary' => 'Prima lettura utile disponibile per questa unità.',
|
||||||
|
'delta' => null,
|
||||||
|
'average' => null,
|
||||||
|
'rule' => 'first_reading',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$delta = round($letturaFine - $letturaInizio, 3);
|
||||||
|
if ($delta < 0) {
|
||||||
|
return [
|
||||||
|
'status' => 'danger',
|
||||||
|
'summary' => 'La lettura inserita è inferiore alla precedente.',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => null,
|
||||||
|
'rule' => 'lower_than_previous',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($delta === 0.0) {
|
||||||
|
return [
|
||||||
|
'status' => 'warning',
|
||||||
|
'summary' => 'La lettura coincide con la precedente: verificare se il contatore è fermo o se serve un controllo.',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => 0.0,
|
||||||
|
'rule' => 'same_as_previous',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$history = StabileServizioLettura::query()
|
||||||
|
->where('stabile_servizio_id', $servizioId)
|
||||||
|
->where('unita_immobiliare_id', $unitaId)
|
||||||
|
->whereNotNull('consumo_valore')
|
||||||
|
->where('consumo_valore', '>', 0)
|
||||||
|
->orderByDesc('periodo_al')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->limit(5)
|
||||||
|
->pluck('consumo_valore')
|
||||||
|
->map(fn($value): float => (float) $value)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($history === []) {
|
||||||
|
return [
|
||||||
|
'status' => 'ok',
|
||||||
|
'summary' => 'Lettura registrata: manca ancora uno storico sufficiente per confronti automatici.',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => null,
|
||||||
|
'rule' => 'no_history',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$average = round(array_sum($history) / count($history), 3);
|
||||||
|
if ($average > 0 && $delta > max(30.0, $average * 3)) {
|
||||||
|
return [
|
||||||
|
'status' => 'warning',
|
||||||
|
'summary' => 'La lettura produce un consumo molto più alto della media storica (' . number_format($delta, 3, ',', '.') . ' mc contro media ' . number_format($average, 3, ',', '.') . ' mc).',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => $average,
|
||||||
|
'rule' => 'much_higher_than_average',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($average >= 1 && $delta < max(0.5, $average * 0.2)) {
|
||||||
|
return [
|
||||||
|
'status' => 'warning',
|
||||||
|
'summary' => 'La lettura produce un consumo molto basso rispetto alla media storica (' . number_format($delta, 3, ',', '.') . ' mc contro media ' . number_format($average, 3, ',', '.') . ' mc).',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => $average,
|
||||||
|
'rule' => 'much_lower_than_average',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'ok',
|
||||||
|
'summary' => 'Lettura coerente con lo storico recente.',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => $average,
|
||||||
|
'rule' => 'within_expected_range',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
private function resolveUnitReaderLabel(UnitaImmobiliare $unita): string
|
private function resolveUnitReaderLabel(UnitaImmobiliare $unita): string
|
||||||
{
|
{
|
||||||
$role = $unita->rubricaRuoliAttivi
|
$role = $unita->rubricaRuoliAttivi
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace App\Filament\Pages\Supporto;
|
namespace App\Filament\Pages\Supporto;
|
||||||
|
|
||||||
use App\Models\Scadenza;
|
use App\Models\Scadenza;
|
||||||
|
|
@ -24,9 +23,9 @@ class TicketAcquaGenerale extends Page
|
||||||
{
|
{
|
||||||
use WithFileUploads;
|
use WithFileUploads;
|
||||||
|
|
||||||
protected static ?string $navigationLabel = 'Acqua Generale';
|
protected static ?string $navigationLabel = 'Contatori Comuni Acqua';
|
||||||
|
|
||||||
protected static ?string $title = 'Acqua Generale';
|
protected static ?string $title = 'Contatori Comuni Acqua';
|
||||||
|
|
||||||
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-building-office-2';
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-building-office-2';
|
||||||
|
|
||||||
|
|
@ -268,6 +267,9 @@ public function getSelectedServiceSnapshotProperty(): ?array
|
||||||
'codice_cliente' => trim((string) ($servizio->codice_cliente ?? '')),
|
'codice_cliente' => trim((string) ($servizio->codice_cliente ?? '')),
|
||||||
'codice_contratto' => trim((string) ($servizio->codice_contratto ?? '')),
|
'codice_contratto' => trim((string) ($servizio->codice_contratto ?? '')),
|
||||||
'matricola' => trim((string) ($servizio->contatore_matricola ?? '')),
|
'matricola' => trim((string) ($servizio->contatore_matricola ?? '')),
|
||||||
|
'common_asset' => trim((string) data_get($servizio->meta, 'common_asset_label', '')),
|
||||||
|
'counter_scope' => trim((string) data_get($servizio->meta, 'counter_scope', '')),
|
||||||
|
'served_area' => trim((string) data_get($servizio->meta, 'served_area_label', '')),
|
||||||
'acea_sms_preview' => count($smsParts) === 3 ? implode('#', $smsParts) : '',
|
'acea_sms_preview' => count($smsParts) === 3 ? implode('#', $smsParts) : '',
|
||||||
'year_folder' => (string) Carbon::parse((string) ($this->dataLettura ?: now()->toDateString()))->format('Y'),
|
'year_folder' => (string) Carbon::parse((string) ($this->dataLettura ?: now()->toDateString()))->format('Y'),
|
||||||
];
|
];
|
||||||
|
|
@ -360,6 +362,7 @@ public function registraLetturaGenerale(): void
|
||||||
$letturaInizio = $previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null;
|
$letturaInizio = $previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null;
|
||||||
$letturaFine = round((float) $validated['letturaAttuale'], 3);
|
$letturaFine = round((float) $validated['letturaAttuale'], 3);
|
||||||
$consumo = $letturaInizio !== null ? round($letturaFine - $letturaInizio, 3) : null;
|
$consumo = $letturaInizio !== null ? round($letturaFine - $letturaInizio, 3) : null;
|
||||||
|
$consistency = $this->buildReadingConsistencyCheck((int) $servizio->id, null, $letturaFine, $letturaInizio);
|
||||||
|
|
||||||
if ($consumo !== null && $consumo < 0) {
|
if ($consumo !== null && $consumo < 0) {
|
||||||
$this->addError('letturaAttuale', 'La lettura attuale non puo essere inferiore alla precedente del contatore generale.');
|
$this->addError('letturaAttuale', 'La lettura attuale non puo essere inferiore alla precedente del contatore generale.');
|
||||||
|
|
@ -456,6 +459,7 @@ public function registraLetturaGenerale(): void
|
||||||
'sms_payload' => $snapshot['acea_sms_preview'] ?? null,
|
'sms_payload' => $snapshot['acea_sms_preview'] ?? null,
|
||||||
'my_acea_url' => 'https://www.aceaato2.it',
|
'my_acea_url' => 'https://www.aceaato2.it',
|
||||||
],
|
],
|
||||||
|
'consistency_check' => $consistency,
|
||||||
'notes' => trim((string) ($validated['noteGenerali'] ?? '')),
|
'notes' => trim((string) ($validated['noteGenerali'] ?? '')),
|
||||||
'photos' => $photoAssets,
|
'photos' => $photoAssets,
|
||||||
],
|
],
|
||||||
|
|
@ -470,10 +474,18 @@ public function registraLetturaGenerale(): void
|
||||||
$this->loadLastGeneralReading();
|
$this->loadLastGeneralReading();
|
||||||
|
|
||||||
Notification::make()
|
Notification::make()
|
||||||
->title('Lettura generale registrata')
|
->title('Lettura contatore comune registrata')
|
||||||
->body('Protocollo ' . $protocol . ' salvato per il contratto selezionato.')
|
->body('Protocollo ' . $protocol . ' salvato per il contratto selezionato.')
|
||||||
->success()
|
->success()
|
||||||
->send();
|
->send();
|
||||||
|
|
||||||
|
if (($consistency['status'] ?? 'ok') !== 'ok') {
|
||||||
|
Notification::make()
|
||||||
|
->title('Controllo coerenza lettura')
|
||||||
|
->body((string) ($consistency['summary'] ?? 'La lettura va verificata rispetto allo storico.'))
|
||||||
|
->warning()
|
||||||
|
->send();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function loadStabileOptions(): void
|
private function loadStabileOptions(): void
|
||||||
|
|
@ -524,9 +536,22 @@ private function loadServiceOptions(): void
|
||||||
->where('attivo', true)
|
->where('attivo', true)
|
||||||
->orderBy('nome')
|
->orderBy('nome')
|
||||||
->orderBy('codice_utenza')
|
->orderBy('codice_utenza')
|
||||||
->get(['id', 'nome', 'codice_utenza', 'codice_cliente', 'codice_contratto', 'contatore_matricola'])
|
->get(['id', 'nome', 'codice_utenza', 'codice_cliente', 'codice_contratto', 'contatore_matricola', 'meta'])
|
||||||
->map(function (StabileServizio $servizio): array {
|
->map(function (StabileServizio $servizio): array {
|
||||||
$parts = [trim((string) ($servizio->nome ?: 'Contratto acqua #' . $servizio->id))];
|
$parts = [trim((string) ($servizio->nome ?: 'Contratto acqua #' . $servizio->id))];
|
||||||
|
$commonAsset = trim((string) data_get($servizio->meta, 'common_asset_label', ''));
|
||||||
|
$servedArea = trim((string) data_get($servizio->meta, 'served_area_label', ''));
|
||||||
|
$counterType = trim((string) data_get($servizio->meta, 'counter_scope', ''));
|
||||||
|
|
||||||
|
if ($commonAsset !== '') {
|
||||||
|
$parts[] = $commonAsset;
|
||||||
|
}
|
||||||
|
if ($servedArea !== '') {
|
||||||
|
$parts[] = $servedArea;
|
||||||
|
}
|
||||||
|
if ($counterType !== '') {
|
||||||
|
$parts[] = 'Contatore ' . $this->formatCounterScopeLabel($counterType);
|
||||||
|
}
|
||||||
if (filled($servizio->codice_contratto)) {
|
if (filled($servizio->codice_contratto)) {
|
||||||
$parts[] = 'Contratto ' . trim((string) $servizio->codice_contratto);
|
$parts[] = 'Contratto ' . trim((string) $servizio->codice_contratto);
|
||||||
}
|
}
|
||||||
|
|
@ -539,7 +564,7 @@ private function loadServiceOptions(): void
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $servizio->id,
|
'id' => (int) $servizio->id,
|
||||||
'label' => implode(' - ', array_filter($parts)),
|
'label' => implode(' - ', array_unique(array_filter($parts))),
|
||||||
];
|
];
|
||||||
})
|
})
|
||||||
->values()
|
->values()
|
||||||
|
|
@ -803,6 +828,105 @@ private function inferReaderType(): string
|
||||||
return $this->rilevatoreTipo ?: 'amministrazione';
|
return $this->rilevatoreTipo ?: 'amministrazione';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{status:string,summary:string,delta:?float,average:?float,rule:?string}
|
||||||
|
*/
|
||||||
|
private function buildReadingConsistencyCheck(int $servizioId, ?int $unitaId, float $letturaFine, ?float $letturaInizio): array
|
||||||
|
{
|
||||||
|
if ($letturaInizio === null) {
|
||||||
|
return [
|
||||||
|
'status' => 'ok',
|
||||||
|
'summary' => 'Prima lettura utile disponibile per questo contatore.',
|
||||||
|
'delta' => null,
|
||||||
|
'average' => null,
|
||||||
|
'rule' => 'first_reading',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$delta = round($letturaFine - $letturaInizio, 3);
|
||||||
|
if ($delta < 0) {
|
||||||
|
return [
|
||||||
|
'status' => 'danger',
|
||||||
|
'summary' => 'La lettura inserita e inferiore alla precedente.',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => null,
|
||||||
|
'rule' => 'lower_than_previous',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($delta === 0.0) {
|
||||||
|
return [
|
||||||
|
'status' => 'warning',
|
||||||
|
'summary' => 'La lettura coincide con la precedente: verificare se il contatore e fermo o se serve un controllo.',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => 0.0,
|
||||||
|
'rule' => 'same_as_previous',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$history = StabileServizioLettura::query()
|
||||||
|
->where('stabile_servizio_id', $servizioId)
|
||||||
|
->when($unitaId === null, fn($query) => $query->whereNull('unita_immobiliare_id'))
|
||||||
|
->when($unitaId !== null, fn($query) => $query->where('unita_immobiliare_id', (int) $unitaId))
|
||||||
|
->whereNotNull('consumo_valore')
|
||||||
|
->where('consumo_valore', '>', 0)
|
||||||
|
->orderByDesc('periodo_al')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->limit(5)
|
||||||
|
->pluck('consumo_valore')
|
||||||
|
->map(fn($value): float => (float) $value)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($history === []) {
|
||||||
|
return [
|
||||||
|
'status' => 'ok',
|
||||||
|
'summary' => 'Lettura registrata: manca ancora uno storico sufficiente per confronti automatici.',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => null,
|
||||||
|
'rule' => 'no_history',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$average = round(array_sum($history) / count($history), 3);
|
||||||
|
if ($average > 0 && $delta > max(30.0, $average * 3)) {
|
||||||
|
return [
|
||||||
|
'status' => 'warning',
|
||||||
|
'summary' => 'La lettura produce un consumo molto piu alto della media storica (' . number_format($delta, 3, ',', '.') . ' mc contro media ' . number_format($average, 3, ',', '.') . ' mc).',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => $average,
|
||||||
|
'rule' => 'much_higher_than_average',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($average >= 1 && $delta < max(0.5, $average * 0.2)) {
|
||||||
|
return [
|
||||||
|
'status' => 'warning',
|
||||||
|
'summary' => 'La lettura produce un consumo molto basso rispetto alla media storica (' . number_format($delta, 3, ',', '.') . ' mc contro media ' . number_format($average, 3, ',', '.') . ' mc).',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => $average,
|
||||||
|
'rule' => 'much_lower_than_average',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'ok',
|
||||||
|
'summary' => 'Lettura coerente con lo storico recente.',
|
||||||
|
'delta' => $delta,
|
||||||
|
'average' => $average,
|
||||||
|
'rule' => 'within_expected_range',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatCounterScopeLabel(string $value): string
|
||||||
|
{
|
||||||
|
return match (strtolower(trim($value))) {
|
||||||
|
'generale' => 'generale',
|
||||||
|
'particolare' => 'particolare',
|
||||||
|
'assente' => 'senza contatore',
|
||||||
|
default => trim($value) !== '' ? trim($value) : 'n/d',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private function syncGeneralWaterScadenze(StabileServizioLettura $reading, StabileServizio $servizio, string $readerLabel): void
|
private function syncGeneralWaterScadenze(StabileServizioLettura $reading, StabileServizio $servizio, string $readerLabel): void
|
||||||
{
|
{
|
||||||
if (! Schema::hasTable('scadenze')) {
|
if (! Schema::hasTable('scadenze')) {
|
||||||
|
|
|
||||||
|
|
@ -17,9 +17,9 @@
|
||||||
|
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Servizi / Utenze - ACQUA</h1>
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Servizi / Beni comuni - ACQUA</h1>
|
||||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||||
Cruscotto operativo acqua con controllo fatture, versamenti/letture condomini e tariffe storicizzate. I dati mostrati seguono lo stabile attivo e l'anno gestione del topbar.
|
Cruscotto operativo per utenze, beni comuni, locali condivisi e contatori acqua dello stabile. Qui convivono contratti, servizi ad uso comune, rimborsi spesa, contatori generali e contatori particolari con storico e tariffe.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-2 inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 dark:border-blue-700/50 dark:bg-blue-900/20 dark:text-blue-200">
|
<div class="mt-2 inline-flex items-center rounded-full border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700 dark:border-blue-700/50 dark:bg-blue-900/20 dark:text-blue-200">
|
||||||
Anno gestione attivo: {{ $annoGestioneAttivo }}
|
Anno gestione attivo: {{ $annoGestioneAttivo }}
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@
|
||||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<h2 class="text-lg font-semibold text-slate-900">Lettura contatore generale acqua</h2>
|
<h2 class="text-lg font-semibold text-slate-900">Lettura contatori comuni acqua</h2>
|
||||||
<div class="mt-1 text-sm text-slate-600">Pagina operativa per autoletture Acea del contatore generale, con foto geolocalizzate, storico per contratto e scadenze da calendario per amministrazione o collaboratori.</div>
|
<div class="mt-1 text-sm text-slate-600">Pagina operativa per contatore generale e contatori particolari dei beni comuni dello stabile, con foto geolocalizzate, storico per contratto e scadenze da calendario per amministrazione o collaboratori.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-full bg-sky-50 px-3 py-1 text-xs font-semibold text-sky-800">Bozza {{ $generalWaterDraftProtocol }}</div>
|
<div class="rounded-full bg-sky-50 px-3 py-1 text-xs font-semibold text-sky-800">Bozza {{ $generalWaterDraftProtocol }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
||||||
@endforeach
|
@endforeach
|
||||||
</select>
|
</select>
|
||||||
<div class="mt-1 text-[11px] text-slate-500">Se lo stabile ha piu contratti, la lettura generale va agganciata a quello corretto.</div>
|
<div class="mt-1 text-[11px] text-slate-500">Se lo stabile ha piu contratti o contatori dedicati a giardino, locale caldaia o altri beni comuni, aggancia la lettura al servizio corretto.</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label class="block text-sm">
|
<label class="block text-sm">
|
||||||
|
|
@ -69,11 +69,14 @@
|
||||||
<div class="text-sm font-semibold text-emerald-900">Riferimenti contratto selezionato</div>
|
<div class="text-sm font-semibold text-emerald-900">Riferimenti contratto selezionato</div>
|
||||||
<div class="mt-3 grid gap-3 md:grid-cols-2 text-sm text-emerald-900">
|
<div class="mt-3 grid gap-3 md:grid-cols-2 text-sm text-emerald-900">
|
||||||
<div><span class="font-medium">Servizio:</span> {{ $service['nome'] ?: 'n/d' }}</div>
|
<div><span class="font-medium">Servizio:</span> {{ $service['nome'] ?: 'n/d' }}</div>
|
||||||
|
<div><span class="font-medium">Bene comune:</span> {{ $service['common_asset'] ?: 'n/d' }}</div>
|
||||||
<div><span class="font-medium">Fornitore:</span> {{ $service['fornitore'] ?: 'n/d' }}</div>
|
<div><span class="font-medium">Fornitore:</span> {{ $service['fornitore'] ?: 'n/d' }}</div>
|
||||||
|
<div><span class="font-medium">Tipo contatore:</span> {{ $service['counter_scope'] ?: 'n/d' }}</div>
|
||||||
<div><span class="font-medium">Codice utenza:</span> {{ $service['codice_utenza'] ?: 'n/d' }}</div>
|
<div><span class="font-medium">Codice utenza:</span> {{ $service['codice_utenza'] ?: 'n/d' }}</div>
|
||||||
<div><span class="font-medium">Codice cliente:</span> {{ $service['codice_cliente'] ?: 'n/d' }}</div>
|
<div><span class="font-medium">Codice cliente:</span> {{ $service['codice_cliente'] ?: 'n/d' }}</div>
|
||||||
<div><span class="font-medium">Codice contratto:</span> {{ $service['codice_contratto'] ?: 'n/d' }}</div>
|
<div><span class="font-medium">Codice contratto:</span> {{ $service['codice_contratto'] ?: 'n/d' }}</div>
|
||||||
<div><span class="font-medium">Matricola contatore:</span> {{ $service['matricola'] ?: 'n/d' }}</div>
|
<div><span class="font-medium">Matricola contatore:</span> {{ $service['matricola'] ?: 'n/d' }}</div>
|
||||||
|
<div><span class="font-medium">Area servita:</span> {{ $service['served_area'] ?: 'n/d' }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 rounded-xl border border-emerald-300 bg-white p-3 text-xs text-slate-700">
|
<div class="mt-3 rounded-xl border border-emerald-300 bg-white p-3 text-xs text-slate-700">
|
||||||
<div class="font-semibold text-slate-900">Formato rapido Acea</div>
|
<div class="font-semibold text-slate-900">Formato rapido Acea</div>
|
||||||
|
|
@ -96,7 +99,7 @@
|
||||||
<input type="date" wire:model.defer="dataLettura" class="w-full rounded-lg border-sky-300 bg-white text-slate-900" />
|
<input type="date" wire:model.defer="dataLettura" class="w-full rounded-lg border-sky-300 bg-white text-slate-900" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 text-xs text-slate-600">La lettura viene salvata nello storico del contratto selezionato, separata dalle singole unita ma con lo stesso standard foto e metadati.</div>
|
<div class="mt-2 text-xs text-slate-600">La lettura viene salvata nello storico del contratto selezionato, separata dalle singole unita ma con lo stesso standard foto e metadati. Se il valore e fuori linea rispetto allo storico recente compare subito un avviso.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||||
|
|
@ -287,12 +290,12 @@
|
||||||
}"
|
}"
|
||||||
x-on:ticket-acqua-generale-draft-reset.window="clearAll()"
|
x-on:ticket-acqua-generale-draft-reset.window="clearAll()"
|
||||||
>
|
>
|
||||||
<span class="mb-2 block font-medium">Foto contatore generale</span>
|
<span class="mb-2 block font-medium">Foto contatore comune</span>
|
||||||
<label class="inline-flex cursor-pointer items-center rounded-md bg-emerald-600 px-3 py-2 text-xs font-medium text-white hover:bg-emerald-500">
|
<label class="inline-flex cursor-pointer items-center rounded-md bg-emerald-600 px-3 py-2 text-xs font-medium text-white hover:bg-emerald-500">
|
||||||
Scatta o aggiungi foto
|
Scatta o aggiungi foto
|
||||||
<input type="file" wire:model="pendingGeneralWaterPhotos" multiple accept="image/*" capture="environment" class="hidden" x-on:change="addFiles($event)" />
|
<input type="file" wire:model="pendingGeneralWaterPhotos" multiple accept="image/*" capture="environment" class="hidden" x-on:change="addFiles($event)" />
|
||||||
</label>
|
</label>
|
||||||
<div class="mt-1 text-xs text-gray-500">Puoi accodare fino a 4 immagini del contatore generale e della targhetta matricola.</div>
|
<div class="mt-1 text-xs text-gray-500">Puoi accodare fino a 4 immagini del contatore generale o particolare e della targhetta matricola.</div>
|
||||||
<div wire:loading wire:target="pendingGeneralWaterPhotos" class="mt-2 text-xs font-medium text-amber-700">Caricamento foto in corso.</div>
|
<div wire:loading wire:target="pendingGeneralWaterPhotos" class="mt-2 text-xs font-medium text-amber-700">Caricamento foto in corso.</div>
|
||||||
|
|
||||||
@if(count($this->selectedGeneralWaterUploads) > 0)
|
@if(count($this->selectedGeneralWaterUploads) > 0)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user