586 lines
21 KiB
PHP
586 lines
21 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 static function canAccess(): bool
|
|
{
|
|
$user = Auth::user();
|
|
|
|
return $user instanceof User
|
|
&& $user->hasAnyRole(['super-admin', 'admin', 'amministratore', 'collaboratore']);
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->dataLettura = now()->toDateString();
|
|
$this->resetWaterDraftUploadState();
|
|
$this->loadStabileOptions();
|
|
$this->bootstrapDefaultSelection();
|
|
$this->loadServiceOptions();
|
|
$this->loadUnitOptions();
|
|
$this->loadPreviousReading();
|
|
}
|
|
|
|
public function updatedStabileId(): void
|
|
{
|
|
$this->loadServiceOptions();
|
|
$this->loadUnitOptions();
|
|
$this->loadPreviousReading();
|
|
}
|
|
|
|
public function updatedServizioId(): void
|
|
{
|
|
$this->loadPreviousReading();
|
|
}
|
|
|
|
public function updatedUnitaId(): void
|
|
{
|
|
$this->loadPreviousReading();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* @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)->inspectUpload($file);
|
|
$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;
|
|
}
|
|
|
|
$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'],
|
|
]);
|
|
|
|
$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()
|
|
->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;
|
|
|
|
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);
|
|
$stored = app(TicketAttachmentUploadService::class)->store(
|
|
$file,
|
|
'ticket-acqua/' . $servizio->id . '/' . $unita->id,
|
|
[
|
|
'stored_basename' => pathinfo($this->buildDraftUploadDisplayName($protocol, (string) $file->getClientOriginalName()), PATHINFO_FILENAME),
|
|
'display_name' => $this->buildDraftUploadDisplayName($protocol, (string) $file->getClientOriginalName()),
|
|
]
|
|
);
|
|
|
|
$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' => is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [],
|
|
'description' => trim((string) ($this->waterPhotoDescriptions[$index] ?? '')),
|
|
];
|
|
}
|
|
|
|
$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' => filled($validated['dataLettura'] ?? null)
|
|
? Carbon::parse((string) $validated['dataLettura'])->toDateString()
|
|
: now()->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' => trim((string) $validated['rilevatoreNome']),
|
|
'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'] ?? '')),
|
|
],
|
|
'consumo_valore' => $consumo,
|
|
'consumo_unita' => 'mc',
|
|
'raw' => [
|
|
'source' => 'ticket_acqua_filament',
|
|
'contact' => trim((string) ($validated['rilevatoreContatto'] ?? '')),
|
|
'note' => trim((string) ($validated['noteGenerali'] ?? '')),
|
|
'photo_assets' => $photoAssets,
|
|
],
|
|
'created_by' => (int) $user->id,
|
|
]);
|
|
|
|
$this->letturaAttuale = null;
|
|
$this->noteGenerali = null;
|
|
$this->dataLettura = now()->toDateString();
|
|
$this->resetWaterDraftUploadState();
|
|
$this->loadPreviousReading();
|
|
$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->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()
|
|
->where('stabile_id', (int) $this->stabileId)
|
|
->where(function ($query): void {
|
|
$query->whereNull('attiva')->orWhere('attiva', true);
|
|
})
|
|
->orderBy('codice_unita')
|
|
->orderBy('id')
|
|
->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);
|
|
}
|
|
|
|
return [
|
|
'id' => (int) $unita->id,
|
|
'label' => implode(' - ', array_filter($bits)),
|
|
];
|
|
})
|
|
->all();
|
|
|
|
$unitIds = array_column($this->unitaOptions, 'id');
|
|
if (! in_array((int) $this->unitaId, $unitIds, true)) {
|
|
$this->unitaId = $unitIds[0] ?? null;
|
|
}
|
|
}
|
|
|
|
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->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;
|
|
}
|
|
|
|
/**
|
|
* @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();
|
|
}
|
|
} |