Add general water reading workflow and fix mobile water flows
This commit is contained in:
parent
554212fd0b
commit
d95fba5740
|
|
@ -1227,7 +1227,7 @@ public function getStableGoogleConnectUrl(): string
|
|||
return route('oauth.google.redirect', [
|
||||
'account_key' => 'stabile-' . ($this->stabile?->id ?? 0) . '-pec',
|
||||
'label' => $label,
|
||||
'return_to' => request()->getRequestUri(),
|
||||
'return_to' => $this->getStabilePostaUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ public function getStableGoogleConnectUrl(): string
|
|||
return route('oauth.google.redirect', [
|
||||
'account_key' => 'stabile-' . ($this->stabile?->id ?? 0) . '-pec',
|
||||
'label' => $label,
|
||||
'return_to' => request()->getRequestUri(),
|
||||
'return_to' => $this->getReturnUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +270,12 @@ public function getTestGoogleConnectUrl(): string
|
|||
return route('oauth.google.redirect', [
|
||||
'account_key' => 'gmail-test',
|
||||
'label' => 'Account Google test',
|
||||
'return_to' => request()->getRequestUri(),
|
||||
'return_to' => $this->getReturnUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function getReturnUrl(): string
|
||||
{
|
||||
return static::getUrl(['stabile_id' => (int) ($this->stabile?->id ?? 0)], panel: 'admin-filament');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,25 @@ class FatturaElettronicaScheda extends Page
|
|||
/** @var array<string,mixed> */
|
||||
public array $acquaPdfPreview = [];
|
||||
|
||||
public function getDocumentoProtocolloConAnnoProperty(): ?string
|
||||
{
|
||||
if (! $this->documento instanceof Documento) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$protocollo = trim((string) ($this->documento->numero_protocollo ?? ''));
|
||||
if ($protocollo === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$year = $this->documento->data_documento?->format('Y')
|
||||
?: $this->documento->created_at?->format('Y')
|
||||
?: $this->fattura->data_fattura?->format('Y')
|
||||
?: now()->format('Y');
|
||||
|
||||
return $year . ' · ' . $protocollo;
|
||||
}
|
||||
|
||||
public function getFornitoreAgganciatoLabel(): ?string
|
||||
{
|
||||
$f = $this->fattura->fornitore;
|
||||
|
|
|
|||
|
|
@ -455,6 +455,8 @@ private function loadServiceOptions(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$this->ensureWaterServiceBucket();
|
||||
|
||||
$this->servizioOptions = StabileServizio::query()
|
||||
->where('stabile_id', (int) $this->stabileId)
|
||||
->where('tipo', 'acqua')
|
||||
|
|
@ -497,8 +499,6 @@ private function loadUnitOptions(): void
|
|||
->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)))];
|
||||
|
|
@ -517,9 +517,10 @@ private function loadUnitOptions(): void
|
|||
}
|
||||
|
||||
$readerLabel = $this->resolveUnitReaderLabel($unita);
|
||||
$searchNames = $this->resolveUnitSearchNames($unita);
|
||||
$searchText = mb_strtolower(implode(' ', array_filter([
|
||||
implode(' ', array_filter($bits)),
|
||||
$readerLabel,
|
||||
...$searchNames,
|
||||
])));
|
||||
|
||||
return [
|
||||
|
|
@ -528,10 +529,15 @@ private function loadUnitOptions(): void
|
|||
'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;
|
||||
|
|
@ -696,6 +702,38 @@ private function syncSuggestedReaderName(): void
|
|||
}
|
||||
}
|
||||
|
||||
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 !== []) {
|
||||
|
|
@ -752,6 +790,54 @@ private function resolveUnitReaderLabel(UnitaImmobiliare $unita): string
|
|||
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([
|
||||
|
|
|
|||
883
app/Filament/Pages/Supporto/TicketAcquaGenerale.php
Normal file
883
app/Filament/Pages/Supporto/TicketAcquaGenerale.php
Normal file
|
|
@ -0,0 +1,883 @@
|
|||
<?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 = 'Acqua Generale';
|
||||
|
||||
protected static ?string $title = 'Acqua Generale';
|
||||
|
||||
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 ?? '')),
|
||||
'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;
|
||||
|
||||
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',
|
||||
],
|
||||
'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 generale registrata')
|
||||
->body('Protocollo ' . $protocol . ' salvato per il contratto selezionato.')
|
||||
->success()
|
||||
->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'])
|
||||
->map(function (StabileServizio $servizio): array {
|
||||
$parts = [trim((string) ($servizio->nome ?: 'Contratto acqua #' . $servizio->id))];
|
||||
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_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';
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
<form method="POST" action="{{ route('oauth.google.disconnect') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="account_key" value="{{ $account['key'] }}" />
|
||||
<input type="hidden" name="return_to" value="{{ request()->getRequestUri() }}" />
|
||||
<input type="hidden" name="return_to" value="{{ $this->getReturnUrl() }}" />
|
||||
<button type="submit" class="rounded-md bg-rose-100 px-2 py-1 text-[11px] font-medium text-rose-700 hover:bg-rose-200">Disconnetti</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@
|
|||
<div class="text-xs text-gray-500 dark:text-gray-400">PDF allegato (se presente) oppure prospetto generato</div>
|
||||
@if($this->documento)
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Protocollo: <span class="font-medium">{{ $this->documento->numero_protocollo }}</span>
|
||||
Protocollo: <span class="font-medium">{{ $this->documentoProtocolloConAnno ?: $this->documento->numero_protocollo }}</span>
|
||||
·
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,375 @@
|
|||
<x-filament-panels::page>
|
||||
<div class="space-y-4">
|
||||
@if ($errors->any())
|
||||
<div class="rounded-xl border border-rose-300 bg-rose-50 p-3 text-sm text-rose-800">
|
||||
<div class="font-semibold">Controlla i dati prima di salvare</div>
|
||||
<ul class="mt-2 list-disc pl-5">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-slate-900">Lettura contatore generale 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>
|
||||
<div class="rounded-full bg-sky-50 px-3 py-1 text-xs font-semibold text-sky-800">Bozza {{ $generalWaterDraftProtocol }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 xl:grid-cols-[1.05fr_0.95fr]">
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Stabile</span>
|
||||
<select wire:model.live="stabileId" class="w-full rounded-lg border-gray-300">
|
||||
<option value="">Seleziona stabile</option>
|
||||
@foreach($stabileOptions as $option)
|
||||
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Contratto / servizio acqua</span>
|
||||
<select wire:model.live="servizioId" class="w-full rounded-lg border-gray-300">
|
||||
<option value="">Seleziona contratto</option>
|
||||
@foreach($servizioOptions as $option)
|
||||
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
||||
@endforeach
|
||||
</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>
|
||||
</label>
|
||||
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Letturista</span>
|
||||
<select wire:model.live="rilevatoreUserId" class="w-full rounded-lg border-gray-300">
|
||||
<option value="">Seleziona letturista</option>
|
||||
@foreach($readerOptions as $option)
|
||||
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Nominativo che invia la lettura</span>
|
||||
<input type="text" wire:model.defer="rilevatoreNome" class="w-full rounded-lg border-gray-300" placeholder="Nome e cognome" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@php($service = $this->selectedServiceSnapshot)
|
||||
@if($service)
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 shadow-sm">
|
||||
<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><span class="font-medium">Servizio:</span> {{ $service['nome'] ?: 'n/d' }}</div>
|
||||
<div><span class="font-medium">Fornitore:</span> {{ $service['fornitore'] ?: '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 contratto:</span> {{ $service['codice_contratto'] ?: 'n/d' }}</div>
|
||||
<div><span class="font-medium">Matricola contatore:</span> {{ $service['matricola'] ?: 'n/d' }}</div>
|
||||
</div>
|
||||
<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="mt-1">SMS a 3399941808: <span class="font-mono">{{ $service['acea_sms_preview'] !== '' ? $service['acea_sms_preview'] : 'codiceutenza#codicecliente#autolettura' }}</span></div>
|
||||
<div class="mt-1">Numero verde: <span class="font-medium">800 130 331</span></div>
|
||||
<div class="mt-1">Area clienti: <span class="font-medium">www.aceaato2.it</span></div>
|
||||
<div class="mt-1">Cartella archivio anno: <span class="font-medium">{{ $service['year_folder'] }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 shadow-sm">
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium text-slate-900">Lettura effettiva</span>
|
||||
<input type="number" step="0.001" min="0" wire:model.defer="letturaAttuale" class="w-full rounded-lg border-sky-300 bg-white text-2xl font-semibold text-slate-900" placeholder="Es. 1284.350" />
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium text-slate-900">Data lettura</span>
|
||||
<input type="date" wire:model.defer="dataLettura" class="w-full rounded-lg border-sky-300 bg-white text-slate-900" />
|
||||
</label>
|
||||
</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>
|
||||
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="text-sm font-semibold text-slate-900">Finestra Acea e calendario operativo</div>
|
||||
<div class="mt-3 grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Inizio finestra autolettura</span>
|
||||
<input type="date" wire:model.defer="aceaWindowStart" class="w-full rounded-lg border-gray-300" />
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Fine finestra autolettura</span>
|
||||
<input type="date" wire:model.defer="aceaWindowEnd" class="w-full rounded-lg border-gray-300" />
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Data passaggio incaricato Acea</span>
|
||||
<input type="date" wire:model.defer="aceaVisitDate" class="w-full rounded-lg border-gray-300" />
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Canale previsto invio lettura</span>
|
||||
<select wire:model.defer="aceaAutoSendChannel" class="w-full rounded-lg border-gray-300">
|
||||
<option value="manuale">Manuale studio</option>
|
||||
<option value="sms">SMS Acea</option>
|
||||
<option value="myacea">MyAcea Acqua</option>
|
||||
<option value="telefono">Numero verde</option>
|
||||
<option value="api_futura">Invio automatico futuro</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Fascia da</span>
|
||||
<input type="time" wire:model.defer="aceaVisitTimeFrom" class="w-full rounded-lg border-gray-300" />
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Fascia a</span>
|
||||
<input type="time" wire:model.defer="aceaVisitTimeTo" class="w-full rounded-lg border-gray-300" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-slate-500">Quando salvi la lettura, creo anche le scadenze operative per la finestra Acea e per il passaggio dell'incaricato se hai compilato le date.</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Note operative</span>
|
||||
<textarea rows="4" wire:model.defer="noteGenerali" class="w-full rounded-lg border-gray-300" placeholder="Es. accesso locale contatori, due contratti nello stabile, lettura inviata via MyAcea oppure da confermare via SMS..."></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="text-sm font-semibold text-slate-900">Ultima lettura generale nota</div>
|
||||
@if($lastGeneralReading)
|
||||
<div class="mt-3 space-y-1 text-sm text-slate-700">
|
||||
<div><span class="font-medium">Valore:</span> {{ number_format((float) $lastGeneralReading['value'], 3, ',', '.') }}</div>
|
||||
<div><span class="font-medium">Data:</span> {{ $lastGeneralReading['date'] ?: 'n/d' }}</div>
|
||||
<div><span class="font-medium">Letturista:</span> {{ $lastGeneralReading['reader'] ?: 'n/d' }}</div>
|
||||
<div><span class="font-medium">Protocollo:</span> {{ $lastGeneralReading['protocol'] ?: 'n/d' }}</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-3 text-sm text-slate-500">Nessuna lettura generale ancora registrata per questo contratto.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<label class="block text-sm"
|
||||
x-data="{
|
||||
localPreviews: [],
|
||||
localClientMetadata: [],
|
||||
init() {
|
||||
window.__ticketAcquaGeneralePreviewState = window.__ticketAcquaGeneralePreviewState || [];
|
||||
window.__ticketAcquaGeneraleClientMetadataState = window.__ticketAcquaGeneraleClientMetadataState || [];
|
||||
this.localPreviews = [...window.__ticketAcquaGeneralePreviewState];
|
||||
this.localClientMetadata = [...window.__ticketAcquaGeneraleClientMetadataState];
|
||||
this.syncClientMetadataToWire();
|
||||
},
|
||||
persist() {
|
||||
window.__ticketAcquaGeneralePreviewState = [...this.localPreviews];
|
||||
},
|
||||
persistClientMetadata() {
|
||||
window.__ticketAcquaGeneraleClientMetadataState = [...this.localClientMetadata];
|
||||
this.syncClientMetadataToWire();
|
||||
},
|
||||
revoke(items) {
|
||||
items.forEach((item) => {
|
||||
if (item.url) {
|
||||
URL.revokeObjectURL(item.url);
|
||||
}
|
||||
});
|
||||
},
|
||||
syncClientMetadataToWire() {
|
||||
$wire.set('generalWaterClientMetadata', this.localClientMetadata.map((item) => ({
|
||||
source: item.source,
|
||||
name: item.name,
|
||||
size: item.size,
|
||||
captured_at: item.captured_at || null,
|
||||
device: item.device || null,
|
||||
gps_decimal: item.gps_decimal || null,
|
||||
gps_accuracy_meters: item.gps_accuracy_meters || null,
|
||||
})), false);
|
||||
},
|
||||
getDeviceLabel() {
|
||||
if (navigator.userAgentData?.platform) {
|
||||
return navigator.userAgentData.platform;
|
||||
}
|
||||
|
||||
return navigator.platform || navigator.userAgent || '';
|
||||
},
|
||||
async readGeolocation() {
|
||||
if (!navigator.geolocation) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => resolve({
|
||||
gps_decimal: {
|
||||
lat: position.coords.latitude,
|
||||
lng: position.coords.longitude,
|
||||
},
|
||||
gps_accuracy_meters: position.coords.accuracy || null,
|
||||
}),
|
||||
() => resolve(null),
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 30000,
|
||||
timeout: 8000,
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
async buildClientMetadata(file) {
|
||||
const base = {
|
||||
source: 'camera',
|
||||
name: file?.name || '',
|
||||
size: file?.size || 0,
|
||||
captured_at: new Date(file?.lastModified || Date.now()).toISOString(),
|
||||
device: this.getDeviceLabel(),
|
||||
};
|
||||
|
||||
const geo = await this.readGeolocation();
|
||||
|
||||
return geo ? { ...base, ...geo } : base;
|
||||
},
|
||||
mergeClientMetadata(items) {
|
||||
const existing = new Map(this.localClientMetadata.map((item) => [`${item.name}|${item.size}`, item]));
|
||||
items.forEach((item) => {
|
||||
if (!item?.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
existing.set(`${item.name}|${item.size}`, item);
|
||||
});
|
||||
this.localClientMetadata = Array.from(existing.values());
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
async addFiles(event) {
|
||||
const batchId = Date.now();
|
||||
const files = Array.from(event?.target?.files || []);
|
||||
const mapped = files.map((file, index) => ({
|
||||
key: `${batchId}-${index}-${file.name}-${file.size}`,
|
||||
name: file.name,
|
||||
size: file.size || 0,
|
||||
url: (file.type || '').startsWith('image/') ? URL.createObjectURL(file) : null,
|
||||
}));
|
||||
|
||||
const existing = new Map(this.localPreviews.map((item) => [item.key, item]));
|
||||
mapped.forEach((item) => existing.set(item.key, item));
|
||||
this.localPreviews = Array.from(existing.values());
|
||||
this.mergeClientMetadata(await Promise.all(files.map((file) => this.buildClientMetadata(file))));
|
||||
this.persist();
|
||||
|
||||
if (event?.target) {
|
||||
event.target.value = null;
|
||||
}
|
||||
},
|
||||
previewFor(name, size = 0) {
|
||||
const exact = this.localPreviews.find((item) => item.name === name && Number(item.size || 0) === Number(size || 0));
|
||||
return exact?.url || null;
|
||||
},
|
||||
clearAll() {
|
||||
this.revoke(this.localPreviews);
|
||||
this.localPreviews = [];
|
||||
this.localClientMetadata = [];
|
||||
this.persist();
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
}"
|
||||
x-on:ticket-acqua-generale-draft-reset.window="clearAll()"
|
||||
>
|
||||
<span class="mb-2 block font-medium">Foto contatore generale</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">
|
||||
Scatta o aggiungi foto
|
||||
<input type="file" wire:model="pendingGeneralWaterPhotos" multiple accept="image/*" capture="environment" class="hidden" x-on:change="addFiles($event)" />
|
||||
</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 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)
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
@foreach($this->selectedGeneralWaterUploads as $upload)
|
||||
<div class="rounded-xl border bg-slate-50 p-3 text-xs shadow-sm">
|
||||
<div class="mb-2 aspect-[4/3] overflow-hidden rounded-lg border bg-white" x-show="Boolean(@js($upload['preview_url']) || previewFor(@js($upload['original_name']), @js($upload['size'] ?? 0)))">
|
||||
<img x-bind:src="@js($upload['preview_url']) || previewFor(@js($upload['original_name']), @js($upload['size'] ?? 0))" alt="{{ $upload['name'] }}" class="h-full w-full object-cover" />
|
||||
</div>
|
||||
<div class="rounded-lg border bg-white px-3 py-2 text-[11px] text-slate-700">
|
||||
<div class="font-medium">{{ $upload['name'] }}</div>
|
||||
<div class="mt-1 text-emerald-700">Protocollo bozza: {{ $upload['protocol'] }}</div>
|
||||
<div class="mt-1 text-slate-500">Origine: {{ $upload['original_name'] }}</div>
|
||||
</div>
|
||||
@php
|
||||
$metadata = is_array($upload['metadata'] ?? null) ? $upload['metadata'] : [];
|
||||
$gpsLat = data_get($metadata, 'gps_decimal.lat');
|
||||
$gpsLng = data_get($metadata, 'gps_decimal.lng');
|
||||
$hasGps = is_numeric($gpsLat) && is_numeric($gpsLng);
|
||||
$device = trim((string) data_get($metadata, 'device', ''));
|
||||
@endphp
|
||||
@if($hasGps || $device !== '')
|
||||
<div class="mt-2 rounded-lg border border-slate-200 bg-white p-2 text-[11px] text-slate-700">
|
||||
@if($hasGps)
|
||||
<div><span class="font-medium">Coordinate:</span> {{ number_format((float) $gpsLat, 5, '.', '') }}, {{ number_format((float) $gpsLng, 5, '.', '') }}</div>
|
||||
@endif
|
||||
@if($device !== '')
|
||||
<div><span class="font-medium">Dispositivo:</span> {{ $device }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
<label class="mt-2 block">
|
||||
<span class="mb-1 block text-[11px] font-medium">Nota foto</span>
|
||||
<input type="text" wire:model.defer="generalWaterPhotoDescriptions.{{ $upload['index'] }}" class="w-full rounded-lg border-gray-300" placeholder="Es. display principale, targhetta matricola, quadro contatori" />
|
||||
</label>
|
||||
<button type="button" wire:click="removeGeneralWaterPhoto({{ (int) $upload['index'] }})" class="mt-2 inline-flex items-center rounded-md bg-rose-600 px-2 py-1 text-[11px] font-medium text-white hover:bg-rose-500">Rimuovi</button>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-3 rounded-xl border border-dashed border-slate-300 bg-slate-50 px-4 py-6 text-center text-sm text-slate-500">Nessuna foto in coda. Aggiungi almeno la foto display e, se possibile, matricola/contratto esposto vicino al contatore.</div>
|
||||
@endif
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="text-sm font-semibold text-slate-900">Storico rapido contratto</div>
|
||||
@if(count($this->recentGeneralReadings) > 0)
|
||||
<div class="mt-3 space-y-2">
|
||||
@foreach($this->recentGeneralReadings as $row)
|
||||
<div class="rounded-xl border bg-slate-50 px-3 py-2 text-xs text-slate-700">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="font-medium">{{ $row['date'] ?: 'n/d' }} · {{ $row['protocollo'] ?: 'senza protocollo' }}</div>
|
||||
<div>{{ $row['workflow'] ?: 'n/d' }}</div>
|
||||
</div>
|
||||
<div class="mt-1">Valore: <span class="font-medium">{{ $row['value'] !== null ? number_format((float) $row['value'], 3, ',', '.') . ' mc' : 'n/d' }}</span></div>
|
||||
@if($row['consumo'] !== null)
|
||||
<div class="mt-1">Consumo: {{ number_format((float) $row['consumo'], 3, ',', '.') }} mc</div>
|
||||
@endif
|
||||
@if($row['reader'] !== '')
|
||||
<div class="mt-1">Letturista: {{ $row['reader'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
<div class="mt-3 text-sm text-slate-500">Storico ancora vuoto per questo contratto.</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<x-filament::button wire:click="registraLetturaGenerale" wire:loading.attr="disabled" wire:target="registraLetturaGenerale,pendingGeneralWaterPhotos">
|
||||
<span wire:loading.remove wire:target="registraLetturaGenerale">Salva lettura generale</span>
|
||||
<span wire:loading wire:target="registraLetturaGenerale">Salvataggio...</span>
|
||||
</x-filament::button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="mt-1 text-[11px] text-slate-500">Se lo stabile non ha ancora un servizio acqua attivo, preparo automaticamente un archivio letture dedicato.</div>
|
||||
</label>
|
||||
|
||||
<label class="block text-sm md:col-span-2">
|
||||
|
|
|
|||
|
|
@ -378,6 +378,7 @@
|
|||
}));
|
||||
this.appendPreviews(target, mapped);
|
||||
this.mergeClientMetadata(await Promise.all(files.map((file) => this.buildClientMetadata(file, kind))));
|
||||
this.draftSequence += files.length;
|
||||
this.persist();
|
||||
if (event?.target) {
|
||||
event.target.value = null;
|
||||
|
|
@ -401,14 +402,34 @@
|
|||
this.persist();
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
previewFor(draftName, originalName, size = 0) {
|
||||
previewFor(draftName, originalName, size = 0, source = null) {
|
||||
const pool = [...this.localCameraPreviews, ...this.localAttachmentPreviews];
|
||||
const exact = pool.find((item) => item.name === originalName && Number(item.size || 0) === Number(size || 0));
|
||||
const exact = pool.find((item) => {
|
||||
if (source === 'camera' && !String(item.key || '').startsWith('camera-')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source === 'attachment' && !String(item.key || '').startsWith('attachment-')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.name === originalName && Number(item.size || 0) === Number(size || 0);
|
||||
});
|
||||
if (exact?.url) {
|
||||
return exact.url;
|
||||
}
|
||||
|
||||
const match = pool.find((item) => item.draftName === draftName || item.name === originalName);
|
||||
const match = pool.find((item) => {
|
||||
if (source === 'camera' && !String(item.key || '').startsWith('camera-')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (source === 'attachment' && !String(item.key || '').startsWith('attachment-')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.draftName === draftName || item.name === originalName;
|
||||
});
|
||||
|
||||
return match?.url || null;
|
||||
},
|
||||
|
|
@ -465,8 +486,8 @@
|
|||
@foreach($this->selectedUploads as $upload)
|
||||
<div class="rounded-xl border bg-white p-3 text-xs shadow-sm">
|
||||
@if($upload['is_image'])
|
||||
<div class="mb-2 aspect-[4/3] overflow-hidden rounded-lg border bg-slate-50" x-show="Boolean(@js($upload['preview_url']) || previewFor(@js($upload['name']), @js($upload['original_name']), @js($upload['size'] ?? 0)))">
|
||||
<img x-bind:src="@js($upload['preview_url']) || previewFor(@js($upload['name']), @js($upload['original_name']), @js($upload['size'] ?? 0))" alt="{{ $upload['name'] }}" class="h-full w-full object-cover" />
|
||||
<div class="mb-2 aspect-[4/3] overflow-hidden rounded-lg border bg-slate-50" x-show="Boolean(@js($upload['preview_url']) || previewFor(@js($upload['name']), @js($upload['original_name']), @js($upload['size'] ?? 0), @js($upload['metadata']['capture_source'] ?? null)))">
|
||||
<img x-bind:src="@js($upload['preview_url']) || previewFor(@js($upload['name']), @js($upload['original_name']), @js($upload['size'] ?? 0), @js($upload['metadata']['capture_source'] ?? null))" alt="{{ $upload['name'] }}" class="h-full w-full object-cover" />
|
||||
</div>
|
||||
@endif
|
||||
<div class="flex flex-wrap items-center gap-2 rounded-lg border bg-gray-50 px-3 py-2">
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user