netgescon-day0/app/Filament/Pages/Supporto/TicketAcquaGenerale.php

1008 lines
41 KiB
PHP

<?php
namespace App\Filament\Pages\Supporto;
use App\Models\Scadenza;
use App\Models\Stabile;
use App\Models\StabileServizio;
use App\Models\StabileServizioLettura;
use App\Models\User;
use App\Services\Support\TicketAttachmentUploadService;
use App\Support\StabileContext;
use BackedEnum;
use Carbon\Carbon;
use Filament\Notifications\Notification;
use Filament\Pages\Page;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Livewire\WithFileUploads;
use Throwable;
use UnitEnum;
class TicketAcquaGenerale extends Page
{
use WithFileUploads;
protected static ?string $navigationLabel = 'Contatori Comuni Acqua';
protected static ?string $title = 'Contatori Comuni Acqua';
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-building-office-2';
protected static UnitEnum|string|null $navigationGroup = 'Mobile';
protected static ?int $navigationSort = 27;
protected static ?string $slug = 'supporto/ticket-acqua-generale';
protected string $view = 'filament.pages.supporto.ticket-acqua-generale';
public ?int $stabileId = null;
public ?int $servizioId = null;
public ?int $rilevatoreUserId = null;
public ?string $rilevatoreNome = null;
public string $rilevatoreTipo = 'amministrazione';
public ?string $letturaAttuale = null;
public ?string $dataLettura = null;
public ?string $noteGenerali = null;
public ?string $aceaWindowStart = null;
public ?string $aceaWindowEnd = null;
public ?string $aceaVisitDate = null;
public ?string $aceaVisitTimeFrom = '07:30';
public ?string $aceaVisitTimeTo = '18:30';
public string $aceaAutoSendChannel = 'manuale';
/** @var array<int,mixed> */
public array $newGeneralWaterPhotos = [];
/** @var array<int,mixed> */
public array $pendingGeneralWaterPhotos = [];
/** @var array<int,string> */
public array $generalWaterPhotoDescriptions = [];
/** @var array<string,string> */
public array $generalWaterUploadCodes = [];
public string $generalWaterDraftProtocol = '';
public int $generalWaterDraftSequence = 1;
/** @var array<int,array{id:int,label:string}> */
public array $stabileOptions = [];
/** @var array<int,array{id:int,label:string}> */
public array $servizioOptions = [];
/** @var array<int,array{id:int,label:string,tipo:string}> */
public array $readerOptions = [];
/** @var array<string,mixed>|null */
public ?array $lastGeneralReading = null;
/** @var array<int,array<string,mixed>> */
public array $generalWaterClientMetadata = [];
public static function canAccess(): bool
{
$user = Auth::user();
return $user instanceof User
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
}
public function mount(): void
{
$user = Auth::user();
$this->dataLettura = now()->toDateString();
$this->rilevatoreNome = $user instanceof User ? trim((string) $user->name) : null;
$this->resetGeneralWaterDraftUploadState();
$this->loadStabileOptions();
$this->bootstrapDefaultSelection();
$this->loadServiceOptions();
$this->loadReaderOptions();
$this->loadLastGeneralReading();
}
public function updatedStabileId(): void
{
$this->loadServiceOptions();
$this->loadReaderOptions();
$this->loadLastGeneralReading();
}
public function updatedServizioId(): void
{
$this->loadLastGeneralReading();
}
public function updatedRilevatoreUserId(): void
{
foreach ($this->readerOptions as $option) {
if ((int) ($option['id'] ?? 0) !== (int) ($this->rilevatoreUserId ?? 0)) {
continue;
}
$this->rilevatoreNome = (string) ($option['label'] ?? $this->rilevatoreNome);
$this->rilevatoreTipo = (string) ($option['tipo'] ?? $this->rilevatoreTipo);
break;
}
}
public function updatedPendingGeneralWaterPhotos(): void
{
$this->appendPendingGeneralWaterPhotos();
}
public function removeGeneralWaterPhoto(int $index): void
{
$file = $this->newGeneralWaterPhotos[$index] ?? null;
unset($this->newGeneralWaterPhotos[$index]);
$this->newGeneralWaterPhotos = array_values($this->newGeneralWaterPhotos);
if (is_object($file)) {
unset($this->generalWaterUploadCodes[$this->resolveUploadQueueKey($file, $index)]);
}
$this->syncGeneralWaterDescriptionSlots();
}
/**
* @param array<int,array<string,mixed>> $items
*/
public function updateGeneralWaterClientMetadata(array $items): void
{
$this->generalWaterClientMetadata = array_values(array_filter(array_map(function (mixed $item): ?array {
if (! is_array($item)) {
return null;
}
$name = trim((string) ($item['name'] ?? ''));
$size = (int) ($item['size'] ?? 0);
if ($name === '' || $size < 0) {
return null;
}
return [
'source' => trim((string) ($item['source'] ?? '')),
'name' => $name,
'size' => $size,
'captured_at' => trim((string) ($item['captured_at'] ?? '')),
'device' => trim((string) ($item['device'] ?? '')),
'gps_decimal' => is_array($item['gps_decimal'] ?? null) ? $item['gps_decimal'] : [],
'gps_accuracy_meters' => $item['gps_accuracy_meters'] ?? null,
];
}, $items)));
}
/**
* @return array<int,array{index:int,name:string,original_name:string,protocol:string,mime:string,is_image:bool,preview_url:?string,size:int,metadata:array<string,mixed>}>
*/
public function getSelectedGeneralWaterUploadsProperty(): array
{
$rows = [];
foreach ($this->newGeneralWaterPhotos as $index => $file) {
if (! is_object($file)) {
continue;
}
$name = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('foto-' . $index));
$mime = (string) (method_exists($file, 'getClientMimeType') ? $file->getClientMimeType() : 'application/octet-stream');
$size = (int) (method_exists($file, 'getSize') ? $file->getSize() : 0);
$protocol = $this->resolveDraftUploadCode($file, $index);
$metadata = app(TicketAttachmentUploadService::class)->mergeClientCaptureMetadata(
app(TicketAttachmentUploadService::class)->inspectUpload($file),
$this->resolveClientCaptureMetadata($name, $size),
);
$preview = null;
if (method_exists($file, 'temporaryUrl')) {
try {
$preview = (string) $file->temporaryUrl();
} catch (Throwable) {
$preview = null;
}
}
$rows[] = [
'index' => (int) $index,
'name' => $this->buildDraftUploadDisplayName($protocol, $name),
'original_name' => $name,
'protocol' => $protocol,
'mime' => $mime,
'is_image' => str_starts_with($mime, 'image/'),
'preview_url' => $preview,
'size' => $size,
'metadata' => is_array($metadata) ? $metadata : [],
];
}
return $rows;
}
/**
* @return array<string,mixed>|null
*/
public function getSelectedServiceSnapshotProperty(): ?array
{
if (! $this->servizioId) {
return null;
}
$servizio = StabileServizio::query()
->with(['fornitore:id,ragione_sociale'])
->where('id', (int) $this->servizioId)
->where('stabile_id', (int) ($this->stabileId ?? 0))
->first();
if (! $servizio instanceof StabileServizio) {
return null;
}
$smsParts = array_filter([
trim((string) ($servizio->codice_utenza ?? '')),
trim((string) ($servizio->codice_cliente ?? '')),
trim((string) ($this->letturaAttuale ?? '')),
]);
return [
'id' => (int) $servizio->id,
'nome' => trim((string) ($servizio->nome ?? '')),
'fornitore' => trim((string) ($servizio->fornitore?->ragione_sociale ?? '')),
'codice_utenza' => trim((string) ($servizio->codice_utenza ?? '')),
'codice_cliente' => trim((string) ($servizio->codice_cliente ?? '')),
'codice_contratto' => trim((string) ($servizio->codice_contratto ?? '')),
'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) : '',
'year_folder' => (string) Carbon::parse((string) ($this->dataLettura ?: now()->toDateString()))->format('Y'),
];
}
/**
* @return array<int,array<string,mixed>>
*/
public function getRecentGeneralReadingsProperty(): array
{
if (! $this->servizioId) {
return [];
}
return StabileServizioLettura::query()
->where('stabile_servizio_id', (int) $this->servizioId)
->whereNull('unita_immobiliare_id')
->orderByDesc('periodo_al')
->orderByDesc('id')
->limit(6)
->get(['id', 'periodo_al', 'lettura_fine', 'consumo_valore', 'rilevatore_nome', 'workflow_stato', 'protocollo_numero'])
->map(fn(StabileServizioLettura $row): array=> [
'id' => (int) $row->id,
'date' => $row->periodo_al?->format('d/m/Y') ?: optional($row->created_at)->format('d/m/Y'),
'value' => $row->lettura_fine,
'consumo' => $row->consumo_valore,
'reader' => trim((string) ($row->rilevatore_nome ?? '')),
'workflow' => trim((string) ($row->workflow_stato ?? '')),
'protocollo' => trim((string) ($row->protocollo_numero ?? '')),
])
->all();
}
public function registraLetturaGenerale(): void
{
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$validated = $this->validate([
'stabileId' => ['required', 'integer'],
'servizioId' => ['required', 'integer'],
'letturaAttuale' => ['required', 'numeric', 'min:0'],
'dataLettura' => ['required', 'date'],
'rilevatoreNome' => ['required', 'string', 'max:255'],
'rilevatoreTipo' => ['nullable', 'string', 'max:80'],
'aceaWindowStart' => ['nullable', 'date'],
'aceaWindowEnd' => ['nullable', 'date', 'after_or_equal:aceaWindowStart'],
'aceaVisitDate' => ['nullable', 'date'],
'aceaVisitTimeFrom' => ['nullable', 'date_format:H:i'],
'aceaVisitTimeTo' => ['nullable', 'date_format:H:i'],
'noteGenerali' => ['nullable', 'string', 'max:4000'],
'newGeneralWaterPhotos.*' => ['nullable', 'image', 'max:8192'],
], [
'servizioId.required' => 'Seleziona il contratto/servizio acqua da usare per la lettura generale.',
'letturaAttuale.required' => 'Inserisci la lettura attuale del contatore generale.',
'dataLettura.required' => 'Indica la data effettiva della lettura.',
'rilevatoreNome.required' => 'Seleziona o inserisci il nominativo del letturista.',
'aceaWindowEnd.after_or_equal' => 'La fine finestra autolettura non puo essere precedente all\'inizio.',
]);
$servizio = StabileServizio::query()
->where('id', (int) $validated['servizioId'])
->where('stabile_id', (int) $validated['stabileId'])
->where('tipo', 'acqua')
->where('attivo', true)
->first();
if (! $servizio instanceof StabileServizio) {
Notification::make()
->title('Contratto acqua non valido')
->body('Controlla lo stabile e il contratto Acea selezionato prima di registrare la lettura generale.')
->warning()
->send();
return;
}
$readingDate = Carbon::parse((string) $validated['dataLettura']);
$protocol = $this->generateGeneralReadingProtocol($readingDate);
$previous = StabileServizioLettura::query()
->where('stabile_servizio_id', (int) $servizio->id)
->whereNull('unita_immobiliare_id')
->whereNotNull('lettura_fine')
->orderByDesc('periodo_al')
->orderByDesc('id')
->first();
$letturaInizio = $previous?->lettura_fine !== null ? (float) $previous->lettura_fine : null;
$letturaFine = round((float) $validated['letturaAttuale'], 3);
$consumo = $letturaInizio !== null ? round($letturaFine - $letturaInizio, 3) : null;
$consistency = $this->buildReadingConsistencyCheck((int) $servizio->id, null, $letturaFine, $letturaInizio);
if ($consumo !== null && $consumo < 0) {
$this->addError('letturaAttuale', 'La lettura attuale non puo essere inferiore alla precedente del contatore generale.');
return;
}
$storageDirectory = $this->buildGeneralWaterStorageDirectory($servizio, $readingDate);
$photoAssets = [];
foreach ($this->newGeneralWaterPhotos as $index => $file) {
if (! is_object($file) || ! method_exists($file, 'store')) {
continue;
}
$draftCode = $this->resolveDraftUploadCode($file, $index);
$naming = $this->buildGeneralWaterPhotoNaming($servizio, $protocol, $draftCode, (string) $file->getClientOriginalName(), $readingDate);
$stored = app(TicketAttachmentUploadService::class)->store(
$file,
$storageDirectory,
[
'stored_basename' => $naming['stored_basename'],
'display_name' => $naming['display_name'],
]
);
$storedMetadata = app(TicketAttachmentUploadService::class)->mergeClientCaptureMetadata(
is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [],
$this->resolveClientCaptureMetadata(
(string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ''),
(int) (method_exists($file, 'getSize') ? $file->getSize() : 0),
),
);
$photoAssets[] = [
'path' => (string) ($stored['path'] ?? ''),
'original_name' => (string) ($stored['original_name'] ?? ''),
'metadata' => $storedMetadata,
'description' => trim((string) ($this->generalWaterPhotoDescriptions[$index] ?? '')),
'drive_directory' => (string) ($naming['drive_relative_directory'] ?? ''),
'display_name' => (string) ($naming['display_name'] ?? ''),
'draft_protocol' => $draftCode,
];
}
$reference = 'TICKET-ACQUA-GENERALE:' . (int) $servizio->id . ':' . now()->format('YmdHis');
$readerLabel = trim((string) $validated['rilevatoreNome']);
$snapshot = $this->getSelectedServiceSnapshotProperty();
$row = StabileServizioLettura::query()->create([
'stabile_id' => (int) $validated['stabileId'],
'stabile_servizio_id' => (int) $servizio->id,
'fornitore_id' => $servizio->fornitore_id,
'periodo_dal' => $previous?->periodo_al,
'periodo_al' => $readingDate,
'tipologia_lettura' => 'autolettura_contatore_generale',
'canale_acquisizione' => 'filament_ticket_acqua_generale',
'riferimento_acquisizione' => $reference,
'workflow_stato' => 'ricevuta',
'protocollo_categoria' => 'ACQ-GEN',
'protocollo_numero' => $protocol,
'prossima_lettura_scadenza_at' => filled($validated['aceaVisitDate'] ?? null) ? Carbon::parse((string) $validated['aceaVisitDate']) : null,
'deadline_lettura_at' => filled($validated['aceaWindowEnd'] ?? null)
? Carbon::parse((string) $validated['aceaWindowEnd'])
: (filled($validated['aceaVisitDate'] ?? null) ? Carbon::parse((string) $validated['aceaVisitDate']) : null),
'rilevatore_tipo' => trim((string) ($validated['rilevatoreTipo'] ?? '')) ?: $this->inferReaderType(),
'rilevatore_nome' => $readerLabel,
'lettura_precedente_valore' => $letturaInizio,
'lettura_inizio' => $letturaInizio,
'lettura_fine' => $letturaFine,
'lettura_foto_path' => $photoAssets[0]['path'] ?? null,
'lettura_foto_original_name' => $photoAssets[0]['original_name'] ?? null,
'lettura_foto_metadata' => $photoAssets[0]['metadata'] ?? null,
'consumo_valore' => $consumo,
'consumo_unita' => 'mc',
'archivio_documentale_path' => $photoAssets[0]['drive_directory'] ?? null,
'raw' => [
'source' => 'ticket_acqua_generale',
'service_snapshot' => $snapshot,
'reader_user_id' => (int) ($this->rilevatoreUserId ?? 0) ?: null,
'reader_assignment' => [
'user_id' => (int) ($this->rilevatoreUserId ?? 0) ?: null,
'name' => $readerLabel,
'type' => $this->inferReaderType(),
],
'acea_schedule' => [
'window_start' => $validated['aceaWindowStart'] ?? null,
'window_end' => $validated['aceaWindowEnd'] ?? null,
'visit_date' => $validated['aceaVisitDate'] ?? null,
'visit_time_from' => $validated['aceaVisitTimeFrom'] ?? null,
'visit_time_to' => $validated['aceaVisitTimeTo'] ?? null,
'channel' => $this->aceaAutoSendChannel,
],
'acea_submission' => [
'sms_number' => '3399941808',
'call_center' => '800 130 331',
'sms_payload' => $snapshot['acea_sms_preview'] ?? null,
'my_acea_url' => 'https://www.aceaato2.it',
],
'consistency_check' => $consistency,
'notes' => trim((string) ($validated['noteGenerali'] ?? '')),
'photos' => $photoAssets,
],
'created_by' => (int) $user->id,
]);
$this->syncGeneralWaterScadenze($row, $servizio, $readerLabel);
$this->resetGeneralWaterDraftUploadState();
$this->letturaAttuale = null;
$this->noteGenerali = null;
$this->dispatch('ticket-acqua-generale-draft-reset');
$this->loadLastGeneralReading();
Notification::make()
->title('Lettura contatore comune registrata')
->body('Protocollo ' . $protocol . ' salvato per il contratto selezionato.')
->success()
->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
{
$user = Auth::user();
if (! $user instanceof User) {
$this->stabileOptions = [];
return;
}
$this->stabileOptions = StabileContext::accessibleStabili($user)
->map(fn(Stabile $stabile): array=> [
'id' => (int) $stabile->id,
'label' => trim((string) ($stabile->codice_stabile ?? '') . ' - ' . (string) ($stabile->denominazione ?? '')),
])
->values()
->all();
}
private function bootstrapDefaultSelection(): void
{
$user = Auth::user();
if (! $user instanceof User) {
return;
}
$activeId = StabileContext::resolveActiveStabileId($user);
$optionIds = array_column($this->stabileOptions, 'id');
if ($activeId && in_array((int) $activeId, $optionIds, true)) {
$this->stabileId = (int) $activeId;
return;
}
$this->stabileId = $optionIds[0] ?? null;
}
private function loadServiceOptions(): void
{
if (! $this->stabileId) {
$this->servizioOptions = [];
$this->servizioId = null;
return;
}
$this->servizioOptions = StabileServizio::query()
->where('stabile_id', (int) $this->stabileId)
->where('tipo', 'acqua')
->where('attivo', true)
->orderBy('nome')
->orderBy('codice_utenza')
->get(['id', 'nome', 'codice_utenza', 'codice_cliente', 'codice_contratto', 'contatore_matricola', 'meta'])
->map(function (StabileServizio $servizio): array {
$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)) {
$parts[] = 'Contratto ' . trim((string) $servizio->codice_contratto);
}
if (filled($servizio->codice_utenza)) {
$parts[] = 'Utenza ' . trim((string) $servizio->codice_utenza);
}
if (filled($servizio->contatore_matricola)) {
$parts[] = 'Contatore ' . trim((string) $servizio->contatore_matricola);
}
return [
'id' => (int) $servizio->id,
'label' => implode(' - ', array_unique(array_filter($parts))),
];
})
->values()
->all();
$serviceIds = array_column($this->servizioOptions, 'id');
if (! in_array((int) ($this->servizioId ?? 0), $serviceIds, true)) {
$this->servizioId = $serviceIds[0] ?? null;
}
}
private function loadReaderOptions(): void
{
$user = Auth::user();
if (! $user instanceof User || ! $this->stabileId) {
$this->readerOptions = [];
return;
}
$stabile = Stabile::query()
->with(['amministratore.user:id,name', 'collaboratori:id,name'])
->find((int) $this->stabileId);
$options = [];
$push = static function (array &$options, ?User $candidate, string $tipo): void {
if (! $candidate instanceof User) {
return;
}
$options[(int) $candidate->id] = [
'id' => (int) $candidate->id,
'label' => trim((string) $candidate->name),
'tipo' => $tipo,
];
};
$push($options, $user, $user->hasRole('collaboratore') ? 'collaboratore' : 'amministrazione');
$push($options, $stabile?->amministratore?->user, 'amministrazione');
foreach (($stabile?->collaboratori ?? collect()) as $collaboratore) {
$push($options, $collaboratore, 'collaboratore');
}
uasort($options, static fn(array $left, array $right): int => strcasecmp((string) $left['label'], (string) $right['label']));
$this->readerOptions = array_values($options);
$readerIds = array_column($this->readerOptions, 'id');
if (! in_array((int) ($this->rilevatoreUserId ?? 0), $readerIds, true)) {
$this->rilevatoreUserId = (int) $user->id;
$this->updatedRilevatoreUserId();
}
}
private function loadLastGeneralReading(): void
{
$this->lastGeneralReading = null;
if (! $this->servizioId) {
return;
}
$previous = StabileServizioLettura::query()
->where('stabile_servizio_id', (int) $this->servizioId)
->whereNull('unita_immobiliare_id')
->whereNotNull('lettura_fine')
->orderByDesc('periodo_al')
->orderByDesc('id')
->first();
if (! $previous instanceof StabileServizioLettura) {
return;
}
$this->lastGeneralReading = [
'id' => (int) $previous->id,
'date' => $previous->periodo_al?->format('d/m/Y'),
'value' => $previous->lettura_fine,
'reader' => trim((string) ($previous->rilevatore_nome ?? '')),
'workflow' => trim((string) ($previous->workflow_stato ?? '')),
'protocol' => trim((string) ($previous->protocollo_numero ?? '')),
];
}
private function appendPendingGeneralWaterPhotos(): void
{
$pending = array_values(array_filter($this->pendingGeneralWaterPhotos, fn($file) => is_object($file)));
if ($pending === []) {
return;
}
$available = max(0, 4 - count($this->newGeneralWaterPhotos));
if ($available <= 0) {
$this->pendingGeneralWaterPhotos = [];
return;
}
$accepted = array_slice($pending, 0, $available);
$currentTotal = count($this->newGeneralWaterPhotos);
$this->newGeneralWaterPhotos = array_values(array_merge($this->newGeneralWaterPhotos, $accepted));
$this->pendingGeneralWaterPhotos = [];
$this->assignDraftUploadCodes($accepted, $currentTotal);
$this->syncGeneralWaterDescriptionSlots();
}
private function resetGeneralWaterDraftUploadState(): void
{
$this->newGeneralWaterPhotos = [];
$this->pendingGeneralWaterPhotos = [];
$this->generalWaterPhotoDescriptions = [];
$this->generalWaterUploadCodes = [];
$this->generalWaterClientMetadata = [];
$this->generalWaterDraftProtocol = 'TAG-' . now()->format('Ymd-His');
$this->generalWaterDraftSequence = 1;
}
private function syncGeneralWaterDescriptionSlots(): void
{
$next = [];
foreach ($this->newGeneralWaterPhotos as $index => $_file) {
$next[$index] = (string) ($this->generalWaterPhotoDescriptions[$index] ?? '');
}
$this->generalWaterPhotoDescriptions = $next;
}
/**
* @param array<int,mixed> $files
*/
private function assignDraftUploadCodes(array $files, int $fallbackIndexStart): void
{
foreach ($files as $offset => $file) {
if (! is_object($file)) {
continue;
}
$key = $this->resolveUploadQueueKey($file, $fallbackIndexStart + $offset);
if (isset($this->generalWaterUploadCodes[$key])) {
continue;
}
$this->generalWaterUploadCodes[$key] = $this->buildDraftProtocolCode($this->generalWaterDraftSequence);
$this->generalWaterDraftSequence++;
}
}
private function resolveDraftUploadCode(object $file, int $fallbackIndex): string
{
$key = $this->resolveUploadQueueKey($file, $fallbackIndex);
if (! isset($this->generalWaterUploadCodes[$key])) {
$this->generalWaterUploadCodes[$key] = $this->buildDraftProtocolCode($this->generalWaterDraftSequence);
$this->generalWaterDraftSequence++;
}
return $this->generalWaterUploadCodes[$key];
}
private function buildDraftProtocolCode(int $sequence): string
{
return $this->generalWaterDraftProtocol . '-F' . str_pad((string) $sequence, 2, '0', STR_PAD_LEFT);
}
private function buildDraftUploadDisplayName(string $protocol, string $originalName): string
{
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
return $protocol . ($extension !== '' ? '.' . $extension : '');
}
private function resolveUploadQueueKey(object $file, int $fallbackIndex): string
{
if (method_exists($file, 'getFilename')) {
$filename = trim((string) $file->getFilename());
if ($filename !== '') {
return $filename;
}
}
return spl_object_hash($file) ?: 'general-water-upload-' . $fallbackIndex;
}
private function generateGeneralReadingProtocol(Carbon $readingDate): string
{
$year = $readingDate->format('Y');
$next = StabileServizioLettura::query()
->where('tipologia_lettura', 'autolettura_contatore_generale')
->whereYear('created_at', (int) $year)
->count() + 1;
return 'ACQ-GEN-' . $year . '-' . str_pad((string) $next, 4, '0', STR_PAD_LEFT);
}
private function buildGeneralWaterStorageDirectory(StabileServizio $servizio, Carbon $readingDate): string
{
$servizio->loadMissing('stabile:id,denominazione,codice_stabile');
$stabileLabel = trim((string) ($servizio->stabile?->denominazione ?: $servizio->stabile?->codice_stabile ?: ('stabile-' . $servizio->stabile_id)));
return 'condomini/' . $this->normalizeLinuxPathSegment($stabileLabel) . '/utenze/acqua/generale/' . $readingDate->format('Y');
}
/**
* @return array{stored_basename:string,display_name:string,drive_relative_directory:string}
*/
private function buildGeneralWaterPhotoNaming(StabileServizio $servizio, string $protocol, string $draftCode, string $originalName, Carbon $readingDate): array
{
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
$suffix = $extension !== '' ? '.' . $extension : '';
$serviceKey = $this->normalizeLinuxPathSegment((string) ($servizio->codice_contratto ?: $servizio->codice_utenza ?: $servizio->id));
$base = trim(implode('_', array_filter([
'contatore_generale',
$serviceKey,
Str::lower(str_replace(['.', ' '], '-', $protocol)),
Str::lower(str_replace(['.', ' '], '-', $draftCode)),
])), '_');
return [
'stored_basename' => $base !== '' ? $base : ('contatore-generale-' . $readingDate->format('YmdHis')),
'display_name' => $protocol . '-' . $draftCode . $suffix,
'drive_relative_directory' => $this->buildGeneralWaterStorageDirectory($servizio, $readingDate),
];
}
private function normalizeLinuxPathSegment(string $value): string
{
$ascii = (string) Str::of($value)->ascii()->lower();
$normalized = preg_replace('/[^a-z0-9]+/', '_', $ascii) ?? '';
return trim($normalized, '_') !== '' ? trim($normalized, '_') : 'n_d';
}
/**
* @return array<string,mixed>
*/
private function resolveClientCaptureMetadata(string $originalName, int $size): array
{
foreach ($this->generalWaterClientMetadata as $item) {
if (($item['name'] ?? '') === $originalName && (int) ($item['size'] ?? -1) === $size) {
return $item;
}
}
foreach ($this->generalWaterClientMetadata as $item) {
if (($item['name'] ?? '') === $originalName) {
return $item;
}
}
return [];
}
private function inferReaderType(): string
{
foreach ($this->readerOptions as $option) {
if ((int) ($option['id'] ?? 0) === (int) ($this->rilevatoreUserId ?? 0)) {
return (string) ($option['tipo'] ?? '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
{
if (! Schema::hasTable('scadenze')) {
return;
}
$serviceName = trim((string) ($servizio->nome ?: ('Contratto #' . $servizio->id)));
$contractBits = array_filter([
trim((string) ($servizio->codice_contratto ?? '')),
trim((string) ($servizio->codice_utenza ?? '')),
]);
$contractLabel = $contractBits !== [] ? ' [' . implode(' / ', $contractBits) . ']' : '';
if (filled($this->aceaWindowStart)) {
$this->createScadenza([
'stabile_id' => (int) $reading->stabile_id,
'codice_stabile_legacy' => $reading->stabile?->codice_stabile,
'descrizione_sintetica' => 'Finestra autolettura Acea ' . $serviceName . $contractLabel . (filled($this->aceaWindowEnd) ? ' fino al ' . Carbon::parse((string) $this->aceaWindowEnd)->format('d/m/Y') : ''),
'data_scadenza' => Carbon::parse((string) $this->aceaWindowStart)->toDateString(),
'ora_scadenza' => null,
'natura' => 'acqua',
'natura_label' => 'Autolettura acqua generale',
'gestione_tipo' => 'operativa',
'anno' => Carbon::parse((string) ($this->dataLettura ?: now()->toDateString()))->format('Y'),
'richiede_pop_up' => true,
'giorni_anticipo' => 2,
'popup_dal' => Carbon::parse((string) $this->aceaWindowStart)->copy()->subDays(2)->toDateString(),
'legacy_payload' => [
'source' => 'ticket_acqua_generale',
'schedule_type' => 'window',
'stabile_servizio_id' => (int) $servizio->id,
'reading_id' => (int) $reading->id,
'reading_reference' => $reading->riferimento_acquisizione,
'window_end' => $this->aceaWindowEnd,
'assigned_user_id' => (int) ($this->rilevatoreUserId ?? 0) ?: null,
'assigned_user_name' => $readerLabel,
'sms_payload' => data_get($reading->raw, 'acea_submission.sms_payload'),
],
]);
}
if (filled($this->aceaVisitDate)) {
$this->createScadenza([
'stabile_id' => (int) $reading->stabile_id,
'codice_stabile_legacy' => $reading->stabile?->codice_stabile,
'descrizione_sintetica' => 'Passaggio Acea contatore generale ' . $serviceName . $contractLabel . ' fascia ' . trim((string) $this->aceaVisitTimeFrom) . '-' . trim((string) $this->aceaVisitTimeTo),
'data_scadenza' => Carbon::parse((string) $this->aceaVisitDate)->toDateString(),
'ora_scadenza' => $this->aceaVisitTimeFrom,
'natura' => 'acqua',
'natura_label' => 'Passaggio Acea contatore generale',
'gestione_tipo' => 'operativa',
'anno' => Carbon::parse((string) ($this->dataLettura ?: now()->toDateString()))->format('Y'),
'richiede_pop_up' => true,
'giorni_anticipo' => 1,
'popup_dal' => Carbon::parse((string) $this->aceaVisitDate)->copy()->subDay()->toDateString(),
'legacy_payload' => [
'source' => 'ticket_acqua_generale',
'schedule_type' => 'visit',
'stabile_servizio_id' => (int) $servizio->id,
'reading_id' => (int) $reading->id,
'reading_reference' => $reading->riferimento_acquisizione,
'visit_time_from' => $this->aceaVisitTimeFrom,
'visit_time_to' => $this->aceaVisitTimeTo,
'assigned_user_id' => (int) ($this->rilevatoreUserId ?? 0) ?: null,
'assigned_user_name' => $readerLabel,
],
]);
}
}
/**
* @param array<string,mixed> $payload
*/
private function createScadenza(array $payload): void
{
Scadenza::query()->create($payload);
}
}