934 lines
34 KiB
PHP
934 lines
34 KiB
PHP
<?php
|
|
namespace App\Filament\Pages\Supporto;
|
|
|
|
use App\Models\StabileServizio;
|
|
use App\Models\StabileServizioLettura;
|
|
use App\Models\UnitaImmobiliare;
|
|
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\Str;
|
|
use Livewire\WithFileUploads;
|
|
use Throwable;
|
|
use UnitEnum;
|
|
|
|
class TicketAcqua extends Page
|
|
{
|
|
use WithFileUploads;
|
|
|
|
protected static ?string $navigationLabel = 'Ticket Acqua';
|
|
|
|
protected static ?string $title = 'Ticket Acqua';
|
|
|
|
protected static BackedEnum|string|null $navigationIcon = 'heroicon-o-beaker';
|
|
|
|
protected static UnitEnum|string|null $navigationGroup = 'Mobile';
|
|
|
|
protected static ?int $navigationSort = 26;
|
|
|
|
protected static ?string $slug = 'supporto/ticket-acqua';
|
|
|
|
protected string $view = 'filament.pages.supporto.ticket-acqua';
|
|
|
|
public ?int $stabileId = null;
|
|
|
|
public ?int $servizioId = null;
|
|
|
|
public ?int $unitaId = null;
|
|
|
|
public ?string $letturaAttuale = null;
|
|
|
|
public ?string $dataLettura = null;
|
|
|
|
public ?string $rilevatoreNome = null;
|
|
|
|
public ?string $rilevatoreContatto = null;
|
|
|
|
public ?string $noteGenerali = null;
|
|
|
|
/** @var array<int,mixed> */
|
|
public array $newWaterPhotos = [];
|
|
|
|
/** @var array<int,mixed> */
|
|
public array $pendingWaterPhotos = [];
|
|
|
|
/** @var array<int,string> */
|
|
public array $waterPhotoDescriptions = [];
|
|
|
|
/** @var array<string,string> */
|
|
public array $waterUploadCodes = [];
|
|
|
|
public string $waterDraftProtocol = '';
|
|
|
|
public int $waterDraftSequence = 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}> */
|
|
public array $unitaOptions = [];
|
|
|
|
/** @var array<string,mixed>|null */
|
|
public ?array $previousReading = null;
|
|
|
|
public string $unitaSearch = '';
|
|
|
|
public ?string $suggestedReaderName = null;
|
|
|
|
/** @var array<int,array<string,mixed>> */
|
|
public array $waterClientMetadata = [];
|
|
|
|
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->resetWaterDraftUploadState();
|
|
$this->loadStabileOptions();
|
|
$this->bootstrapDefaultSelection();
|
|
$this->loadServiceOptions();
|
|
$this->loadUnitOptions();
|
|
$this->loadPreviousReading();
|
|
$this->syncSuggestedReaderName();
|
|
}
|
|
|
|
public function updatedStabileId(): void
|
|
{
|
|
$this->unitaSearch = '';
|
|
$this->loadServiceOptions();
|
|
$this->loadUnitOptions();
|
|
$this->loadPreviousReading();
|
|
$this->syncSuggestedReaderName();
|
|
}
|
|
|
|
public function updatedServizioId(): void
|
|
{
|
|
$this->loadPreviousReading();
|
|
}
|
|
|
|
public function updatedUnitaId(): void
|
|
{
|
|
$this->loadPreviousReading();
|
|
$this->syncSuggestedReaderName();
|
|
}
|
|
|
|
public function updatedPendingWaterPhotos(): void
|
|
{
|
|
$this->appendPendingWaterPhotos();
|
|
}
|
|
|
|
public function removeWaterPhoto(int $index): void
|
|
{
|
|
$file = $this->newWaterPhotos[$index] ?? null;
|
|
unset($this->newWaterPhotos[$index]);
|
|
$this->newWaterPhotos = array_values($this->newWaterPhotos);
|
|
|
|
if (is_object($file)) {
|
|
unset($this->waterUploadCodes[$this->resolveUploadQueueKey($file, $index)]);
|
|
}
|
|
|
|
$this->syncWaterDescriptionSlots();
|
|
}
|
|
|
|
/**
|
|
* @param array<int,array<string,mixed>> $items
|
|
*/
|
|
public function updateWaterClientMetadata(array $items): void
|
|
{
|
|
$this->waterClientMetadata = 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 getSelectedWaterUploadsProperty(): array
|
|
{
|
|
$rows = [];
|
|
|
|
foreach ($this->newWaterPhotos 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');
|
|
$protocol = $this->resolveDraftUploadCode($file, $index);
|
|
$metadata = app(TicketAttachmentUploadService::class)->mergeClientCaptureMetadata(
|
|
app(TicketAttachmentUploadService::class)->inspectUpload($file),
|
|
$this->resolveClientCaptureMetadata($name, (int) (method_exists($file, 'getSize') ? $file->getSize() : 0)),
|
|
);
|
|
$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' => (int) (method_exists($file, 'getSize') ? $file->getSize() : 0),
|
|
'metadata' => is_array($metadata) ? $metadata : [],
|
|
];
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
public function registraLetturaAcqua(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
return;
|
|
}
|
|
|
|
$this->ensureDefaultSelections();
|
|
if (blank($this->rilevatoreNome)) {
|
|
$this->rilevatoreNome = trim((string) ($this->suggestedReaderName ?: $user->name));
|
|
}
|
|
|
|
$validated = $this->validate([
|
|
'stabileId' => ['required', 'integer'],
|
|
'servizioId' => ['required', 'integer'],
|
|
'unitaId' => ['required', 'integer'],
|
|
'letturaAttuale' => ['required', 'numeric', 'min:0'],
|
|
'dataLettura' => ['nullable', 'date'],
|
|
'rilevatoreNome' => ['required', 'string', 'max:255'],
|
|
'rilevatoreContatto' => ['nullable', 'string', 'max:255'],
|
|
'noteGenerali' => ['nullable', 'string', 'max:2000'],
|
|
'newWaterPhotos.*' => ['nullable', 'image', 'max:8192'],
|
|
], [
|
|
'stabileId.required' => 'Seleziona lo stabile.',
|
|
'servizioId.required' => 'Seleziona il servizio acqua.',
|
|
'unitaId.required' => 'Seleziona o cerca l\'unità immobiliare.',
|
|
'letturaAttuale.required' => 'Inserisci la lettura attuale del contatore.',
|
|
'letturaAttuale.numeric' => 'La lettura attuale deve essere numerica.',
|
|
'rilevatoreNome.required' => 'Inserisci il nominativo del letturista.',
|
|
'newWaterPhotos.*.image' => 'Ogni allegato deve essere una foto valida.',
|
|
'newWaterPhotos.*.max' => 'Ogni foto può pesare al massimo 8 MB.',
|
|
], [
|
|
'stabileId' => 'stabile',
|
|
'servizioId' => 'servizio acqua',
|
|
'unitaId' => 'unità immobiliare',
|
|
'letturaAttuale' => 'lettura attuale',
|
|
'rilevatoreNome' => 'nominativo letturista',
|
|
]);
|
|
|
|
$stabileIds = $this->resolveAccessibleStabileIds($user);
|
|
$servizio = StabileServizio::query()
|
|
->where('id', (int) $validated['servizioId'])
|
|
->where('stabile_id', (int) $validated['stabileId'])
|
|
->whereIn('stabile_id', $stabileIds)
|
|
->where('tipo', 'acqua')
|
|
->first();
|
|
|
|
$unita = UnitaImmobiliare::query()
|
|
->with(['rubricaRuoliAttivi.contatto:id,nome,cognome,ragione_sociale,tipo_contatto', 'nominativiStorici'])
|
|
->where('id', (int) $validated['unitaId'])
|
|
->where('stabile_id', (int) $validated['stabileId'])
|
|
->first();
|
|
|
|
if (! $servizio || ! $unita) {
|
|
Notification::make()
|
|
->title('Selezione acqua non valida')
|
|
->body('Controlla stabile, servizio e unità prima di inviare la lettura.')
|
|
->warning()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
$previous = StabileServizioLettura::query()
|
|
->where('stabile_servizio_id', (int) $servizio->id)
|
|
->where('unita_immobiliare_id', (int) $unita->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;
|
|
$readerName = trim((string) ($validated['rilevatoreNome'] ?? ''));
|
|
$readingDate = filled($validated['dataLettura'] ?? null)
|
|
? Carbon::parse((string) $validated['dataLettura'])
|
|
: now();
|
|
$storageDirectory = $this->buildWaterStorageDirectory($servizio, $readingDate);
|
|
|
|
if ($consumo !== null && $consumo < 0) {
|
|
$this->addError('letturaAttuale', 'La lettura attuale non può essere inferiore alla precedente.');
|
|
|
|
Notification::make()
|
|
->title('Lettura non coerente')
|
|
->body('Il valore inserito è inferiore alla lettura precedente memorizzata.')
|
|
->warning()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
$photoAssets = [];
|
|
foreach ($this->newWaterPhotos as $index => $file) {
|
|
if (! is_object($file) || ! method_exists($file, 'store')) {
|
|
continue;
|
|
}
|
|
|
|
$protocol = $this->resolveDraftUploadCode($file, $index);
|
|
$naming = $this->buildWaterPhotoNaming(
|
|
$unita,
|
|
$this->resolveUnitReaderLabel($unita),
|
|
$protocol,
|
|
(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[] = [
|
|
'index' => $index,
|
|
'protocol' => $protocol,
|
|
'path' => $stored['path'],
|
|
'original_name' => $stored['original_name'] ?? null,
|
|
'source_original_name' => $stored['source_original_name'] ?? null,
|
|
'mime' => $stored['mime'] ?? null,
|
|
'size' => $stored['size'] ?? null,
|
|
'metadata' => $storedMetadata,
|
|
'description' => trim((string) ($this->waterPhotoDescriptions[$index] ?? '')),
|
|
'drive_relative_directory' => $naming['drive_relative_directory'],
|
|
'drive_display_name' => $naming['display_name'],
|
|
'stored_basename' => $naming['stored_basename'],
|
|
];
|
|
}
|
|
|
|
$row = StabileServizioLettura::query()->create([
|
|
'stabile_id' => (int) $servizio->stabile_id,
|
|
'stabile_servizio_id' => (int) $servizio->id,
|
|
'unita_immobiliare_id' => (int) $unita->id,
|
|
'fornitore_id' => $servizio->fornitore_id,
|
|
'periodo_dal' => $previous?->periodo_al,
|
|
'periodo_al' => $readingDate->toDateString(),
|
|
'tipologia_lettura' => 'ticket_acqua_mobile',
|
|
'canale_acquisizione' => 'filament_ticket_acqua',
|
|
'workflow_stato' => 'ricevuta',
|
|
'riferimento_acquisizione' => 'TICKET-ACQUA:' . (int) $servizio->id . ':' . (int) $unita->id . ':' . now()->format('YmdHis'),
|
|
'rilevatore_tipo' => 'operatore_filament',
|
|
'rilevatore_nome' => $readerName,
|
|
'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' => [
|
|
'photos' => $photoAssets,
|
|
'contact' => trim((string) ($validated['rilevatoreContatto'] ?? '')),
|
|
'note' => trim((string) ($validated['noteGenerali'] ?? '')),
|
|
'drive_relative_directory' => $storageDirectory,
|
|
],
|
|
'consumo_valore' => $consumo,
|
|
'consumo_unita' => 'mc',
|
|
'raw' => [
|
|
'source' => 'ticket_acqua_filament',
|
|
'contact' => trim((string) ($validated['rilevatoreContatto'] ?? '')),
|
|
'note' => trim((string) ($validated['noteGenerali'] ?? '')),
|
|
'photo_assets' => $photoAssets,
|
|
'drive_relative_directory' => $storageDirectory,
|
|
'reader_label' => $this->resolveUnitReaderLabel($unita),
|
|
],
|
|
'created_by' => (int) $user->id,
|
|
]);
|
|
|
|
$this->letturaAttuale = null;
|
|
$this->noteGenerali = null;
|
|
$this->dataLettura = now()->toDateString();
|
|
$this->resetWaterDraftUploadState();
|
|
$this->loadPreviousReading();
|
|
$this->syncSuggestedReaderName();
|
|
$this->dispatch('ticket-acqua-draft-reset');
|
|
|
|
Notification::make()
|
|
->title('Ticket acqua registrato')
|
|
->body('Lettura salvata per unità ' . ((string) ($unita->codice_unita ?: ('#' . $unita->id))) . ' con riferimento #' . $row->id . '.')
|
|
->success()
|
|
->send();
|
|
}
|
|
|
|
private function loadStabileOptions(): void
|
|
{
|
|
$user = Auth::user();
|
|
if (! $user instanceof User) {
|
|
$this->stabileOptions = [];
|
|
return;
|
|
}
|
|
|
|
$this->stabileOptions = StabileContext::accessibleStabili($user)
|
|
->sortBy(fn($stabile) => mb_strtolower((string) ($stabile->denominazione ?? '')))
|
|
->map(fn($stabile): array => [
|
|
'id' => (int) $stabile->id,
|
|
'label' => trim((string) (($stabile->codice_stabile ?? '') !== ''
|
|
? ($stabile->codice_stabile . ' - ' . $stabile->denominazione)
|
|
: ($stabile->denominazione ?? ('Stabile #' . $stabile->id)))),
|
|
])
|
|
->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->ensureWaterServiceBucket();
|
|
|
|
$this->servizioOptions = StabileServizio::query()
|
|
->where('stabile_id', (int) $this->stabileId)
|
|
->where('tipo', 'acqua')
|
|
->where('attivo', true)
|
|
->orderBy('nome')
|
|
->get(['id', 'nome', 'codice_utenza', 'contatore_matricola'])
|
|
->map(function (StabileServizio $servizio): array {
|
|
$bits = [trim((string) ($servizio->nome ?: 'Servizio acqua #' . $servizio->id))];
|
|
if (filled($servizio->codice_utenza)) {
|
|
$bits[] = 'Utenza ' . trim((string) $servizio->codice_utenza);
|
|
}
|
|
if (filled($servizio->contatore_matricola)) {
|
|
$bits[] = 'Contatore ' . trim((string) $servizio->contatore_matricola);
|
|
}
|
|
|
|
return [
|
|
'id' => (int) $servizio->id,
|
|
'label' => implode(' - ', array_filter($bits)),
|
|
];
|
|
})
|
|
->all();
|
|
|
|
$serviceIds = array_column($this->servizioOptions, 'id');
|
|
if (! in_array((int) $this->servizioId, $serviceIds, true)) {
|
|
$this->servizioId = $serviceIds[0] ?? null;
|
|
}
|
|
}
|
|
|
|
private function loadUnitOptions(): void
|
|
{
|
|
if (! $this->stabileId) {
|
|
$this->unitaOptions = [];
|
|
$this->unitaId = null;
|
|
return;
|
|
}
|
|
|
|
$this->unitaOptions = UnitaImmobiliare::query()
|
|
->with(['rubricaRuoliAttivi.contatto:id,nome,cognome,ragione_sociale,tipo_contatto', 'nominativiStorici'])
|
|
->where('stabile_id', (int) $this->stabileId)
|
|
->where(function ($query): void {
|
|
$query->whereNull('attiva')->orWhere('attiva', true);
|
|
})
|
|
->get(['id', 'codice_unita', 'denominazione', 'scala', 'piano', 'interno'])
|
|
->map(function (UnitaImmobiliare $unita): array {
|
|
$bits = [trim((string) ($unita->codice_unita ?: ('UI #' . $unita->id)))];
|
|
if (filled($unita->denominazione)) {
|
|
$bits[] = trim((string) $unita->denominazione);
|
|
}
|
|
|
|
$location = array_filter([
|
|
filled($unita->scala) ? 'Scala ' . trim((string) $unita->scala) : null,
|
|
filled($unita->piano) ? 'Piano ' . trim((string) $unita->piano) : null,
|
|
filled($unita->interno) ? 'Int. ' . trim((string) $unita->interno) : null,
|
|
]);
|
|
|
|
if ($location !== []) {
|
|
$bits[] = implode(' ', $location);
|
|
}
|
|
|
|
$readerLabel = $this->resolveUnitReaderLabel($unita);
|
|
$searchNames = $this->resolveUnitSearchNames($unita);
|
|
$searchText = mb_strtolower(implode(' ', array_filter([
|
|
implode(' ', array_filter($bits)),
|
|
...$searchNames,
|
|
])));
|
|
|
|
return [
|
|
'id' => (int) $unita->id,
|
|
'label' => implode(' - ', array_filter($bits)),
|
|
'reader_label' => $readerLabel,
|
|
'folder_prefix' => $this->buildUnitFolderPrefix($unita),
|
|
'search_text' => $searchText,
|
|
'sort_key' => $this->buildUnitNaturalSortKey($unita, $readerLabel),
|
|
];
|
|
})
|
|
->all();
|
|
|
|
usort($this->unitaOptions, static function (array $left, array $right): int {
|
|
return strnatcasecmp((string) ($left['sort_key'] ?? ''), (string) ($right['sort_key'] ?? ''));
|
|
});
|
|
|
|
$unitIds = array_column($this->unitaOptions, 'id');
|
|
if (! in_array((int) $this->unitaId, $unitIds, true)) {
|
|
$this->unitaId = $unitIds[0] ?? null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<int,array{id:int,label:string,reader_label:string,folder_prefix:string,search_text:string}>
|
|
*/
|
|
public function getFilteredUnitaOptionsProperty(): array
|
|
{
|
|
$needle = mb_strtolower(trim($this->unitaSearch));
|
|
if ($needle === '') {
|
|
return $this->unitaOptions;
|
|
}
|
|
|
|
return array_values(array_filter($this->unitaOptions, fn(array $option): bool => str_contains((string) ($option['search_text'] ?? ''), $needle)));
|
|
}
|
|
|
|
private function loadPreviousReading(): void
|
|
{
|
|
$this->previousReading = null;
|
|
|
|
if (! $this->servizioId || ! $this->unitaId) {
|
|
return;
|
|
}
|
|
|
|
$previous = StabileServizioLettura::query()
|
|
->where('stabile_servizio_id', (int) $this->servizioId)
|
|
->where('unita_immobiliare_id', (int) $this->unitaId)
|
|
->whereNotNull('lettura_fine')
|
|
->orderByDesc('periodo_al')
|
|
->orderByDesc('id')
|
|
->first();
|
|
|
|
if (! $previous) {
|
|
return;
|
|
}
|
|
|
|
$this->previousReading = [
|
|
'value' => $previous->lettura_fine,
|
|
'date' => $previous->periodo_al?->format('d/m/Y'),
|
|
'id' => (int) $previous->id,
|
|
];
|
|
}
|
|
|
|
private function appendPendingWaterPhotos(): void
|
|
{
|
|
$pending = array_values(array_filter($this->pendingWaterPhotos, fn($file) => is_object($file)));
|
|
if ($pending === []) {
|
|
return;
|
|
}
|
|
|
|
$available = max(0, 4 - count($this->newWaterPhotos));
|
|
if ($available <= 0) {
|
|
$this->pendingWaterPhotos = [];
|
|
|
|
Notification::make()
|
|
->title('Limite foto raggiunto')
|
|
->body('Puoi tenere in coda al massimo 4 foto per una lettura acqua.')
|
|
->warning()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
$accepted = array_slice($pending, 0, $available);
|
|
$currentTotal = count($this->newWaterPhotos);
|
|
$this->newWaterPhotos = array_values(array_merge($this->newWaterPhotos, $accepted));
|
|
$this->pendingWaterPhotos = [];
|
|
$this->assignDraftUploadCodes($accepted, $currentTotal);
|
|
$this->syncWaterDescriptionSlots();
|
|
}
|
|
|
|
private function resetWaterDraftUploadState(): void
|
|
{
|
|
$this->newWaterPhotos = [];
|
|
$this->pendingWaterPhotos = [];
|
|
$this->waterPhotoDescriptions = [];
|
|
$this->waterUploadCodes = [];
|
|
$this->waterClientMetadata = [];
|
|
$this->waterDraftProtocol = 'TA-' . now()->format('Ymd-His');
|
|
$this->waterDraftSequence = 1;
|
|
}
|
|
|
|
private function syncWaterDescriptionSlots(): void
|
|
{
|
|
$next = [];
|
|
|
|
foreach ($this->newWaterPhotos as $index => $_file) {
|
|
$next[$index] = (string) ($this->waterPhotoDescriptions[$index] ?? '');
|
|
}
|
|
|
|
$this->waterPhotoDescriptions = $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->waterUploadCodes[$key])) {
|
|
continue;
|
|
}
|
|
|
|
$this->waterUploadCodes[$key] = $this->buildDraftProtocolCode($this->waterDraftSequence);
|
|
$this->waterDraftSequence++;
|
|
}
|
|
}
|
|
|
|
private function resolveDraftUploadCode(object $file, int $fallbackIndex): string
|
|
{
|
|
$key = $this->resolveUploadQueueKey($file, $fallbackIndex);
|
|
|
|
if (! isset($this->waterUploadCodes[$key])) {
|
|
$this->waterUploadCodes[$key] = $this->buildDraftProtocolCode($this->waterDraftSequence);
|
|
$this->waterDraftSequence++;
|
|
}
|
|
|
|
return $this->waterUploadCodes[$key];
|
|
}
|
|
|
|
private function buildDraftProtocolCode(int $sequence): string
|
|
{
|
|
return $this->waterDraftProtocol . '-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) ?: 'water-upload-' . $fallbackIndex;
|
|
}
|
|
|
|
private function syncSuggestedReaderName(): void
|
|
{
|
|
$previous = $this->suggestedReaderName;
|
|
$current = $this->resolveCurrentUnitOption();
|
|
|
|
$this->suggestedReaderName = trim((string) ($current['reader_label'] ?? '')) ?: null;
|
|
|
|
if (blank($this->rilevatoreNome) || ($previous !== null && $this->rilevatoreNome === $previous)) {
|
|
$this->rilevatoreNome = $this->suggestedReaderName;
|
|
}
|
|
}
|
|
|
|
private function ensureWaterServiceBucket(): void
|
|
{
|
|
if (! $this->stabileId) {
|
|
return;
|
|
}
|
|
|
|
$hasActiveWaterService = StabileServizio::query()
|
|
->where('stabile_id', (int) $this->stabileId)
|
|
->where('tipo', 'acqua')
|
|
->where('attivo', true)
|
|
->exists();
|
|
|
|
if ($hasActiveWaterService) {
|
|
return;
|
|
}
|
|
|
|
StabileServizio::query()->firstOrCreate(
|
|
[
|
|
'stabile_id' => (int) $this->stabileId,
|
|
'tipo' => 'acqua',
|
|
'nome' => 'Archivio letture acqua',
|
|
],
|
|
[
|
|
'attivo' => true,
|
|
'meta' => [
|
|
'auto_created_by' => 'ticket-acqua',
|
|
'purpose' => 'reading-bucket',
|
|
],
|
|
]
|
|
);
|
|
}
|
|
|
|
private function ensureDefaultSelections(): void
|
|
{
|
|
if (! $this->stabileId && $this->stabileOptions !== []) {
|
|
$this->stabileId = (int) ($this->stabileOptions[0]['id'] ?? 0) ?: null;
|
|
}
|
|
|
|
if (! $this->servizioId && $this->servizioOptions !== []) {
|
|
$this->servizioId = (int) ($this->servizioOptions[0]['id'] ?? 0) ?: null;
|
|
}
|
|
|
|
$filtered = $this->getFilteredUnitaOptionsProperty();
|
|
if (! $this->unitaId && $filtered !== []) {
|
|
$this->unitaId = (int) ($filtered[0]['id'] ?? 0) ?: null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string,mixed>|null
|
|
*/
|
|
private function resolveCurrentUnitOption(): ?array
|
|
{
|
|
foreach ($this->unitaOptions as $option) {
|
|
if ((int) ($option['id'] ?? 0) === (int) ($this->unitaId ?? 0)) {
|
|
return $option;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function resolveUnitReaderLabel(UnitaImmobiliare $unita): string
|
|
{
|
|
$role = $unita->rubricaRuoliAttivi
|
|
?->sortByDesc(fn($item) => (bool) ($item->is_preferito ?? false))
|
|
->first();
|
|
|
|
if ($role?->contatto) {
|
|
$label = trim((string) ($role->contatto->nome_completo ?: $role->contatto->ragione_sociale));
|
|
if ($label !== '') {
|
|
return $label;
|
|
}
|
|
}
|
|
|
|
$historic = $unita->nominativiStorici
|
|
?->sortByDesc(fn($item) => (string) ($item->data_fine ?? '9999-12-31'))
|
|
->sortByDesc(fn($item) => (string) ($item->data_inizio ?? ''))
|
|
->first();
|
|
|
|
$historicLabel = trim((string) ($historic->nominativo ?? ''));
|
|
if ($historicLabel !== '') {
|
|
return $historicLabel;
|
|
}
|
|
|
|
return trim((string) ($unita->denominazione ?? ''));
|
|
}
|
|
|
|
/**
|
|
* @return array<int,string>
|
|
*/
|
|
private function resolveUnitSearchNames(UnitaImmobiliare $unita): array
|
|
{
|
|
$names = [];
|
|
|
|
foreach (($unita->rubricaRuoliAttivi ?? collect()) as $role) {
|
|
$contact = $role->contatto;
|
|
if (! $contact) {
|
|
continue;
|
|
}
|
|
|
|
$label = trim((string) ($contact->nome_completo ?: $contact->ragione_sociale));
|
|
if ($label !== '') {
|
|
$names[] = $label;
|
|
}
|
|
}
|
|
|
|
foreach (($unita->nominativiStorici ?? collect()) as $historic) {
|
|
$label = trim((string) ($historic->nominativo ?? ''));
|
|
if ($label !== '') {
|
|
$names[] = $label;
|
|
}
|
|
}
|
|
|
|
$fallback = trim((string) ($unita->denominazione ?? ''));
|
|
if ($fallback !== '') {
|
|
$names[] = $fallback;
|
|
}
|
|
|
|
return array_values(array_unique(array_filter($names)));
|
|
}
|
|
|
|
private function buildUnitNaturalSortKey(UnitaImmobiliare $unita, string $readerLabel): string
|
|
{
|
|
$parts = array_filter([
|
|
trim((string) ($unita->scala ?? '')),
|
|
trim((string) ($unita->interno ?? '')),
|
|
trim((string) ($unita->codice_unita ?? '')),
|
|
trim((string) ($unita->piano ?? '')),
|
|
trim($readerLabel),
|
|
(string) $unita->id,
|
|
]);
|
|
|
|
return implode(' ', $parts);
|
|
}
|
|
|
|
private function buildUnitFolderPrefix(UnitaImmobiliare $unita): string
|
|
{
|
|
$parts = array_filter([
|
|
filled($unita->scala) ? Str::upper(trim((string) $unita->scala)) : null,
|
|
filled($unita->interno) ? trim((string) $unita->interno) : null,
|
|
! filled($unita->interno) && filled($unita->codice_unita) ? trim((string) $unita->codice_unita) : null,
|
|
]);
|
|
|
|
$raw = implode('_', $parts);
|
|
$normalized = preg_replace('/[^A-Za-z0-9_\-]+/', '_', $raw) ?? '';
|
|
|
|
return trim($normalized, '_-') !== '' ? trim($normalized, '_-') : ('UI_' . (int) $unita->id);
|
|
}
|
|
|
|
private function buildWaterStorageDirectory(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)));
|
|
$stabileSlug = $this->normalizeLinuxPathSegment($stabileLabel);
|
|
|
|
return 'condomini/' . $stabileSlug . '/utenze/acqua/letture/' . $readingDate->format('Y');
|
|
}
|
|
|
|
/**
|
|
* @return array{stored_basename:string,display_name:string,drive_relative_directory:string}
|
|
*/
|
|
private function buildWaterPhotoNaming(UnitaImmobiliare $unita, string $readerLabel, string $protocol, string $originalName, Carbon $readingDate): array
|
|
{
|
|
$unita->loadMissing('stabile:id,denominazione,codice_stabile');
|
|
|
|
$extension = strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION));
|
|
$suffix = $extension !== '' ? '.' . $extension : '';
|
|
$readerSafe = $this->normalizeLinuxPathSegment($readerLabel !== '' ? $readerLabel : ('ui-' . $unita->id));
|
|
$folderPrefix = Str::lower($this->buildUnitFolderPrefix($unita));
|
|
$storedBase = trim($folderPrefix . '_' . $readerSafe . '_' . Str::lower(str_replace(['.', ' '], '-', $protocol)), '_');
|
|
|
|
$displayParts = array_filter([
|
|
filled($unita->interno) ? 'Int. ' . trim((string) $unita->interno) : null,
|
|
$readerLabel !== '' ? $readerLabel : null,
|
|
]);
|
|
$displayName = implode(' ', $displayParts);
|
|
if ($displayName === '') {
|
|
$displayName = trim((string) ($unita->codice_unita ?: ('UI ' . $unita->id)));
|
|
}
|
|
|
|
return [
|
|
'stored_basename' => trim($storedBase, '_-') !== '' ? trim($storedBase, '_-') : ('water-photo-' . $unita->id . '-' . $readingDate->format('YmdHis')),
|
|
'display_name' => $displayName . $suffix,
|
|
'drive_relative_directory' => 'condomini/' . $this->normalizeLinuxPathSegment((string) ($unita->stabile?->denominazione ?? 'stabile')) . '/utenze/acqua/letture/' . $readingDate->format('Y'),
|
|
];
|
|
}
|
|
|
|
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->waterClientMetadata as $item) {
|
|
if (($item['name'] ?? '') === $originalName && (int) ($item['size'] ?? -1) === $size) {
|
|
return $item;
|
|
}
|
|
}
|
|
|
|
foreach ($this->waterClientMetadata as $item) {
|
|
if (($item['name'] ?? '') === $originalName) {
|
|
return $item;
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @return array<int,int>
|
|
*/
|
|
private function resolveAccessibleStabileIds(User $user): array
|
|
{
|
|
return StabileContext::accessibleStabili($user)
|
|
->pluck('id')
|
|
->map(fn($value) => (int) $value)
|
|
->filter(fn(int $value) => $value > 0)
|
|
->values()
|
|
->all();
|
|
}
|
|
} |