Add camera metadata fallback and refine ticket acqua
This commit is contained in:
parent
ab2b3af2fa
commit
755e3a5015
|
|
@ -79,6 +79,13 @@ class TicketAcqua extends Page
|
|||
/** @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();
|
||||
|
|
@ -89,20 +96,25 @@ public static function canAccess(): bool
|
|||
|
||||
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
|
||||
|
|
@ -113,6 +125,7 @@ public function updatedServizioId(): void
|
|||
public function updatedUnitaId(): void
|
||||
{
|
||||
$this->loadPreviousReading();
|
||||
$this->syncSuggestedReaderName();
|
||||
}
|
||||
|
||||
public function updatedPendingWaterPhotos(): void
|
||||
|
|
@ -133,6 +146,34 @@ public function removeWaterPhoto(int $index): void
|
|||
$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>}>
|
||||
*/
|
||||
|
|
@ -148,7 +189,10 @@ public function getSelectedWaterUploadsProperty(): array
|
|||
$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);
|
||||
$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')) {
|
||||
|
|
@ -182,6 +226,11 @@ public function registraLetturaAcqua(): void
|
|||
return;
|
||||
}
|
||||
|
||||
$this->ensureDefaultSelections();
|
||||
if (blank($this->rilevatoreNome)) {
|
||||
$this->rilevatoreNome = trim((string) ($this->suggestedReaderName ?: $user->name));
|
||||
}
|
||||
|
||||
$validated = $this->validate([
|
||||
'stabileId' => ['required', 'integer'],
|
||||
'servizioId' => ['required', 'integer'],
|
||||
|
|
@ -192,6 +241,21 @@ public function registraLetturaAcqua(): void
|
|||
'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);
|
||||
|
|
@ -203,6 +267,7 @@ public function registraLetturaAcqua(): void
|
|||
->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();
|
||||
|
|
@ -228,6 +293,11 @@ public function registraLetturaAcqua(): void
|
|||
$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.');
|
||||
|
|
@ -248,14 +318,28 @@ public function registraLetturaAcqua(): void
|
|||
}
|
||||
|
||||
$protocol = $this->resolveDraftUploadCode($file, $index);
|
||||
$naming = $this->buildWaterPhotoNaming(
|
||||
$unita,
|
||||
$this->resolveUnitReaderLabel($unita),
|
||||
$protocol,
|
||||
(string) $file->getClientOriginalName(),
|
||||
$readingDate,
|
||||
);
|
||||
$stored = app(TicketAttachmentUploadService::class)->store(
|
||||
$file,
|
||||
'ticket-acqua/' . $servizio->id . '/' . $unita->id,
|
||||
$storageDirectory,
|
||||
[
|
||||
'stored_basename' => pathinfo($this->buildDraftUploadDisplayName($protocol, (string) $file->getClientOriginalName()), PATHINFO_FILENAME),
|
||||
'display_name' => $this->buildDraftUploadDisplayName($protocol, (string) $file->getClientOriginalName()),
|
||||
'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,
|
||||
|
|
@ -265,8 +349,11 @@ public function registraLetturaAcqua(): void
|
|||
'source_original_name' => $stored['source_original_name'] ?? null,
|
||||
'mime' => $stored['mime'] ?? null,
|
||||
'size' => $stored['size'] ?? null,
|
||||
'metadata' => is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [],
|
||||
'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'],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -276,15 +363,13 @@ public function registraLetturaAcqua(): void
|
|||
'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(),
|
||||
'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' => trim((string) $validated['rilevatoreNome']),
|
||||
'rilevatore_nome' => $readerName,
|
||||
'lettura_precedente_valore' => $letturaInizio,
|
||||
'lettura_inizio' => $letturaInizio,
|
||||
'lettura_fine' => $letturaFine,
|
||||
|
|
@ -294,6 +379,7 @@ public function registraLetturaAcqua(): void
|
|||
'photos' => $photoAssets,
|
||||
'contact' => trim((string) ($validated['rilevatoreContatto'] ?? '')),
|
||||
'note' => trim((string) ($validated['noteGenerali'] ?? '')),
|
||||
'drive_relative_directory' => $storageDirectory,
|
||||
],
|
||||
'consumo_valore' => $consumo,
|
||||
'consumo_unita' => 'mc',
|
||||
|
|
@ -302,6 +388,8 @@ public function registraLetturaAcqua(): void
|
|||
'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,
|
||||
]);
|
||||
|
|
@ -311,6 +399,7 @@ public function registraLetturaAcqua(): void
|
|||
$this->dataLettura = now()->toDateString();
|
||||
$this->resetWaterDraftUploadState();
|
||||
$this->loadPreviousReading();
|
||||
$this->syncSuggestedReaderName();
|
||||
$this->dispatch('ticket-acqua-draft-reset');
|
||||
|
||||
Notification::make()
|
||||
|
|
@ -403,6 +492,7 @@ private function loadUnitOptions(): void
|
|||
}
|
||||
|
||||
$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);
|
||||
|
|
@ -426,9 +516,18 @@ private function loadUnitOptions(): void
|
|||
$bits[] = implode(' ', $location);
|
||||
}
|
||||
|
||||
$readerLabel = $this->resolveUnitReaderLabel($unita);
|
||||
$searchText = mb_strtolower(implode(' ', array_filter([
|
||||
implode(' ', array_filter($bits)),
|
||||
$readerLabel,
|
||||
])));
|
||||
|
||||
return [
|
||||
'id' => (int) $unita->id,
|
||||
'label' => implode(' - ', array_filter($bits)),
|
||||
'id' => (int) $unita->id,
|
||||
'label' => implode(' - ', array_filter($bits)),
|
||||
'reader_label' => $readerLabel,
|
||||
'folder_prefix' => $this->buildUnitFolderPrefix($unita),
|
||||
'search_text' => $searchText,
|
||||
];
|
||||
})
|
||||
->all();
|
||||
|
|
@ -439,6 +538,19 @@ private function loadUnitOptions(): void
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
|
@ -500,6 +612,7 @@ private function resetWaterDraftUploadState(): void
|
|||
$this->pendingWaterPhotos = [];
|
||||
$this->waterPhotoDescriptions = [];
|
||||
$this->waterUploadCodes = [];
|
||||
$this->waterClientMetadata = [];
|
||||
$this->waterDraftProtocol = 'TA-' . now()->format('Ymd-His');
|
||||
$this->waterDraftSequence = 1;
|
||||
}
|
||||
|
|
@ -571,6 +684,155 @@ private function resolveUploadQueueKey(object $file, int $fallbackIndex): string
|
|||
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 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 ?? ''));
|
||||
}
|
||||
|
||||
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>
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -85,6 +85,9 @@ class TicketMobile extends Page
|
|||
/** @var array<string, string> */
|
||||
public array $newTicketUploadCodes = [];
|
||||
|
||||
/** @var array<int,array<string,mixed>> */
|
||||
public array $newTicketClientMetadata = [];
|
||||
|
||||
public string $newTicketDraftProtocol = '';
|
||||
|
||||
public int $newTicketDraftSequence = 1;
|
||||
|
|
@ -682,6 +685,34 @@ public function removeNewTicketFile(int $index): void
|
|||
$this->syncAttachmentDescriptionSlots();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,array<string,mixed>> $items
|
||||
*/
|
||||
public function updateClientCaptureMetadata(array $items): void
|
||||
{
|
||||
$this->newTicketClientMetadata = 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,description:string,size:int,metadata:array<string,mixed>}>
|
||||
*/
|
||||
|
|
@ -703,6 +734,15 @@ public function getSelectedUploadsProperty(): array
|
|||
$metadata = $isImage
|
||||
? app(TicketAttachmentUploadService::class)->inspectUpload($file)
|
||||
: [];
|
||||
$clientMetadata = $isImage
|
||||
? $this->resolveClientCaptureMetadata($index, $name, $size)
|
||||
: [];
|
||||
$metadata = $isImage
|
||||
? app(TicketAttachmentUploadService::class)->mergeClientCaptureMetadata(
|
||||
is_array($metadata) ? $metadata : [],
|
||||
$clientMetadata,
|
||||
)
|
||||
: [];
|
||||
|
||||
$previewUrl = null;
|
||||
if ($isImage && method_exists($file, 'temporaryUrl')) {
|
||||
|
|
@ -765,6 +805,15 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
|
|||
'stored_basename' => pathinfo($displayName, PATHINFO_FILENAME),
|
||||
'display_name' => $displayName,
|
||||
]);
|
||||
$clientMetadata = $this->resolveClientCaptureMetadata(
|
||||
(int) $index,
|
||||
(string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ''),
|
||||
(int) (method_exists($file, 'getSize') ? $file->getSize() : 0),
|
||||
);
|
||||
$storedMetadata = app(TicketAttachmentUploadService::class)->mergeClientCaptureMetadata(
|
||||
is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [],
|
||||
$clientMetadata,
|
||||
);
|
||||
|
||||
$payload = [
|
||||
'ticket_id' => (int) $ticket->id,
|
||||
|
|
@ -776,14 +825,14 @@ private function salvaAllegatiTicket(Ticket $ticket, int $userId): int
|
|||
'size' => $stored['size'],
|
||||
'description' => $this->resolveAttachmentDescription(
|
||||
(int) $index,
|
||||
is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [],
|
||||
$storedMetadata,
|
||||
$stored['original_name'] ?? null,
|
||||
$stored['source_original_name'] ?? null,
|
||||
),
|
||||
];
|
||||
|
||||
if ($supportsAttachmentMetadata) {
|
||||
$payload['metadata'] = is_array($stored['metadata'] ?? null) ? $stored['metadata'] : [];
|
||||
$payload['metadata'] = $storedMetadata;
|
||||
}
|
||||
|
||||
TicketAttachment::query()->create($payload);
|
||||
|
|
@ -916,6 +965,7 @@ private function resetDraftUploadState(): void
|
|||
$this->pendingTicketAttachments = [];
|
||||
$this->newTicketAttachmentDescriptions = [];
|
||||
$this->newTicketUploadCodes = [];
|
||||
$this->newTicketClientMetadata = [];
|
||||
$this->newTicketDraftProtocol = 'TM-' . now()->format('Ymd-His');
|
||||
$this->newTicketDraftSequence = 1;
|
||||
}
|
||||
|
|
@ -979,6 +1029,36 @@ private function resolveUploadQueueKey(object $file, int $fallbackIndex): string
|
|||
return spl_object_hash($file) ?: 'upload-' . $fallbackIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function resolveClientCaptureMetadata(int $index, string $originalName, int $size): array
|
||||
{
|
||||
$source = $index < count($this->newTicketCameraShots) ? 'camera' : 'attachment';
|
||||
|
||||
foreach ($this->newTicketClientMetadata as $item) {
|
||||
if (($item['source'] ?? '') !== $source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($item['name'] ?? '') === $originalName && (int) ($item['size'] ?? -1) === $size) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->newTicketClientMetadata as $item) {
|
||||
if (($item['source'] ?? '') !== $source) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($item['name'] ?? '') === $originalName) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function buildStoredUploadDisplayName(Ticket $ticket, int $index, object $file): string
|
||||
{
|
||||
$originalName = (string) (method_exists($file, 'getClientOriginalName') ? $file->getClientOriginalName() : ('file-' . $index));
|
||||
|
|
|
|||
|
|
@ -26,6 +26,60 @@ public function inspectStoredPath(string $path): array
|
|||
return $this->extractImageMetadata($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $metadata
|
||||
* @param array<string,mixed> $clientMetadata
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function mergeClientCaptureMetadata(array $metadata, array $clientMetadata): array
|
||||
{
|
||||
if ($clientMetadata === []) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$merged = $metadata;
|
||||
|
||||
$captureSource = trim((string) ($clientMetadata['source'] ?? ''));
|
||||
if ($captureSource !== '') {
|
||||
$merged['capture_source'] = $captureSource;
|
||||
}
|
||||
|
||||
$capturedAt = $this->normalizeClientCapturedAt($clientMetadata['captured_at'] ?? null);
|
||||
if (! filled($merged['exif_datetime'] ?? null) && $capturedAt !== null) {
|
||||
$merged['exif_datetime'] = $capturedAt;
|
||||
}
|
||||
|
||||
$device = trim((string) ($clientMetadata['device'] ?? ''));
|
||||
if (! filled($merged['device'] ?? null) && $device !== '') {
|
||||
$merged['device'] = $device;
|
||||
}
|
||||
|
||||
$lat = data_get($clientMetadata, 'gps_decimal.lat');
|
||||
$lng = data_get($clientMetadata, 'gps_decimal.lng');
|
||||
if (! isset($merged['gps_decimal']) && is_numeric($lat) && is_numeric($lng)) {
|
||||
$merged['gps_decimal'] = [
|
||||
'lat' => round((float) $lat, 6),
|
||||
'lng' => round((float) $lng, 6),
|
||||
];
|
||||
$merged['google_maps_url'] = 'https://maps.google.com/?q=' . $merged['gps_decimal']['lat'] . ',' . $merged['gps_decimal']['lng'];
|
||||
}
|
||||
|
||||
$accuracy = $clientMetadata['gps_accuracy_meters'] ?? null;
|
||||
if (is_numeric($accuracy)) {
|
||||
$merged['gps_accuracy_meters'] = (int) round((float) $accuracy);
|
||||
}
|
||||
|
||||
$merged['client_capture'] = array_filter([
|
||||
'source' => $captureSource !== '' ? $captureSource : null,
|
||||
'captured_at' => $capturedAt,
|
||||
'device' => $device !== '' ? $device : null,
|
||||
'gps_decimal' => is_array($merged['gps_decimal'] ?? null) ? $merged['gps_decimal'] : null,
|
||||
'gps_accuracy_meters' => is_numeric($accuracy) ? (int) round((float) $accuracy) : null,
|
||||
], fn($value) => $value !== null && $value !== '');
|
||||
|
||||
return $merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UploadedFile|TemporaryUploadedFile $file
|
||||
* @return array{path:string,mime:string,size:int,original_name:string}
|
||||
|
|
@ -330,6 +384,22 @@ private function extractImageMetadataFromAbsolutePath(string $absolutePath): arr
|
|||
return $metadata;
|
||||
}
|
||||
|
||||
private function normalizeClientCapturedAt(mixed $value): ?string
|
||||
{
|
||||
$raw = trim((string) $value);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$date = new \DateTimeImmutable($raw);
|
||||
|
||||
return $date->format('Y:m:d H:i:s');
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private function gpsCoordinateToDecimal(mixed $coordinate, mixed $reference): ?float
|
||||
{
|
||||
if (! is_array($coordinate) || count($coordinate) < 3) {
|
||||
|
|
|
|||
|
|
@ -45,14 +45,21 @@
|
|||
</select>
|
||||
</label>
|
||||
|
||||
<label class="block text-sm md:col-span-2">
|
||||
<span class="mb-1 block font-medium">Cerca unità immobiliare</span>
|
||||
<input type="text" wire:model.live.debounce.250ms="unitaSearch" class="w-full rounded-lg border-gray-300" placeholder="Es. Int. 10, Maggiore, B 4, codice unità..." />
|
||||
<div class="mt-1 text-[11px] text-slate-500">Filtra l'elenco per interno, nominativo o codice unità.</div>
|
||||
</label>
|
||||
|
||||
<label class="block text-sm md:col-span-2">
|
||||
<span class="mb-1 block font-medium">Unità immobiliare</span>
|
||||
<select wire:model.live="unitaId" class="w-full rounded-lg border-gray-300">
|
||||
<option value="">Seleziona unità</option>
|
||||
@foreach($unitaOptions as $option)
|
||||
@foreach($this->filteredUnitaOptions as $option)
|
||||
<option value="{{ $option['id'] }}">{{ $option['label'] }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<div class="mt-1 text-[11px] text-slate-500">Risultati attuali: {{ count($this->filteredUnitaOptions) }}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -68,8 +75,11 @@
|
|||
<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">Rilevatore</span>
|
||||
<span class="mb-1 block font-medium">Nominativo letturista</span>
|
||||
<input type="text" wire:model.defer="rilevatoreNome" class="w-full rounded-lg border-gray-300" placeholder="Nome operatore o condomino" />
|
||||
@if(filled($suggestedReaderName))
|
||||
<div class="mt-1 text-[11px] text-slate-500">Suggerimento unità: {{ $suggestedReaderName }}</div>
|
||||
@endif
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium">Contatto</span>
|
||||
|
|
@ -101,13 +111,21 @@
|
|||
<label class="block text-sm"
|
||||
x-data="{
|
||||
localWaterPreviews: [],
|
||||
localWaterClientMetadata: [],
|
||||
init() {
|
||||
window.__ticketAcquaPreviewState = window.__ticketAcquaPreviewState || [];
|
||||
window.__ticketAcquaClientMetadataState = window.__ticketAcquaClientMetadataState || [];
|
||||
this.localWaterPreviews = [...window.__ticketAcquaPreviewState];
|
||||
this.localWaterClientMetadata = [...window.__ticketAcquaClientMetadataState];
|
||||
this.syncClientMetadataToWire();
|
||||
},
|
||||
persist() {
|
||||
window.__ticketAcquaPreviewState = [...this.localWaterPreviews];
|
||||
},
|
||||
persistClientMetadata() {
|
||||
window.__ticketAcquaClientMetadataState = [...this.localWaterClientMetadata];
|
||||
this.syncClientMetadataToWire();
|
||||
},
|
||||
revoke(items) {
|
||||
items.forEach((item) => {
|
||||
if (item.url) {
|
||||
|
|
@ -115,9 +133,76 @@
|
|||
}
|
||||
});
|
||||
},
|
||||
addFiles(event) {
|
||||
syncClientMetadataToWire() {
|
||||
$wire.set('waterClientMetadata', this.localWaterClientMetadata.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.localWaterClientMetadata.map((item) => [`${item.name}|${item.size}`, item]));
|
||||
items.forEach((item) => {
|
||||
if (!item?.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
existing.set(`${item.name}|${item.size}`, item);
|
||||
});
|
||||
this.localWaterClientMetadata = Array.from(existing.values());
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
async addFiles(event) {
|
||||
const batchId = Date.now();
|
||||
const mapped = Array.from(event?.target?.files || []).map((file, index) => ({
|
||||
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,
|
||||
|
|
@ -127,6 +212,7 @@
|
|||
const existing = new Map(this.localWaterPreviews.map((item) => [item.key, item]));
|
||||
mapped.forEach((item) => existing.set(item.key, item));
|
||||
this.localWaterPreviews = Array.from(existing.values());
|
||||
this.mergeClientMetadata(await Promise.all(files.map((file) => this.buildClientMetadata(file))));
|
||||
this.persist();
|
||||
|
||||
if (event?.target) {
|
||||
|
|
@ -141,7 +227,9 @@
|
|||
clearAll() {
|
||||
this.revoke(this.localWaterPreviews);
|
||||
this.localWaterPreviews = [];
|
||||
this.localWaterClientMetadata = [];
|
||||
this.persist();
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
}"
|
||||
x-on:ticket-acqua-draft-reset.window="clearAll()"
|
||||
|
|
@ -155,6 +243,27 @@
|
|||
<div wire:loading wire:target="pendingWaterPhotos" class="mt-2 text-xs font-medium text-amber-700">Caricamento foto in corso: preparo la coda della lettura acqua.</div>
|
||||
|
||||
@if(count($this->selectedWaterUploads) > 0)
|
||||
@php($focusUpload = $this->selectedWaterUploads[0] ?? null)
|
||||
@if($focusUpload)
|
||||
<div class="mt-4 rounded-2xl border border-sky-200 bg-sky-50 p-3">
|
||||
<div class="mb-2 text-xs font-semibold text-sky-900">Chicca lettura rapida</div>
|
||||
<div class="text-[11px] text-sky-800">Ti evidenzio la zona del display del contatore e ti lascio il campo lettura già dentro il riquadro operativo.</div>
|
||||
<div class="mt-3 grid gap-3 lg:grid-cols-[1.1fr_0.9fr]">
|
||||
<div class="relative overflow-hidden rounded-xl border bg-white">
|
||||
<img src="{{ $focusUpload['preview_url'] ?: '' }}" x-bind:src="@js($focusUpload['preview_url']) || previewFor(@js($focusUpload['original_name']), @js($focusUpload['size'] ?? 0))" alt="{{ $focusUpload['name'] }}" class="h-full w-full object-cover" />
|
||||
<div class="pointer-events-none absolute inset-y-[30%] right-[11%] w-[32%] rounded-xl border-2 border-amber-400 bg-amber-100/40 shadow-[0_0_0_9999px_rgba(15,23,42,0.10)]"></div>
|
||||
</div>
|
||||
<div class="rounded-xl border bg-white p-3">
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block font-medium text-slate-900">Lettura attuale del contatore</span>
|
||||
<input type="number" step="0.001" min="0" wire:model.defer="letturaAttuale" class="w-full rounded-lg border-amber-300 bg-amber-50 text-2xl font-semibold text-slate-900" placeholder="Es. 1284.350" />
|
||||
</label>
|
||||
<div class="mt-2 text-[11px] text-slate-600">Il riquadro giallo indica l'area delle cifre da leggere nella foto principale.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
@foreach($this->selectedWaterUploads as $upload)
|
||||
<div class="rounded-xl border bg-slate-50 p-3 text-xs shadow-sm">
|
||||
|
|
@ -165,6 +274,9 @@
|
|||
<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>
|
||||
@if(!empty($upload['metadata']['capture_source']))
|
||||
<div class="mt-1 text-slate-500">Sorgente: {{ $upload['metadata']['capture_source'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@php
|
||||
$metadata = is_array($upload['metadata'] ?? null) ? $upload['metadata'] : [];
|
||||
|
|
|
|||
|
|
@ -239,10 +239,14 @@
|
|||
draftSequence: {{ (int) $newTicketDraftSequence }},
|
||||
localCameraPreviews: [],
|
||||
localAttachmentPreviews: [],
|
||||
localClientMetadata: [],
|
||||
init() {
|
||||
window.__ticketMobilePreviewState = window.__ticketMobilePreviewState || { camera: [], attachment: [] };
|
||||
window.__ticketMobileClientMetadataState = window.__ticketMobileClientMetadataState || [];
|
||||
this.localCameraPreviews = [...(window.__ticketMobilePreviewState.camera || [])];
|
||||
this.localAttachmentPreviews = [...(window.__ticketMobilePreviewState.attachment || [])];
|
||||
this.localClientMetadata = [...window.__ticketMobileClientMetadataState];
|
||||
this.syncClientMetadataToWire();
|
||||
},
|
||||
persist() {
|
||||
window.__ticketMobilePreviewState = {
|
||||
|
|
@ -250,6 +254,10 @@
|
|||
attachment: [...this.localAttachmentPreviews],
|
||||
};
|
||||
},
|
||||
persistClientMetadata() {
|
||||
window.__ticketMobileClientMetadataState = [...this.localClientMetadata];
|
||||
this.syncClientMetadataToWire();
|
||||
},
|
||||
revoke(items) {
|
||||
items.forEach((item) => {
|
||||
if (item.url) {
|
||||
|
|
@ -257,6 +265,17 @@
|
|||
}
|
||||
});
|
||||
},
|
||||
syncClientMetadataToWire() {
|
||||
$wire.set('newTicketClientMetadata', 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);
|
||||
},
|
||||
buildDraftName(file, sequence) {
|
||||
const sourceName = file?.name || 'file';
|
||||
const extension = sourceName.includes('.') ? sourceName.split('.').pop().toLowerCase() : '';
|
||||
|
|
@ -284,10 +303,72 @@
|
|||
|
||||
this[target] = Array.from(existing.values());
|
||||
},
|
||||
setPreviews(event, kind) {
|
||||
mergeClientMetadata(items) {
|
||||
const existing = new Map(this.localClientMetadata.map((item) => [`${item.source}|${item.name}|${item.size}`, item]));
|
||||
|
||||
items.forEach((item) => {
|
||||
if (!item?.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
existing.set(`${item.source}|${item.name}|${item.size}`, item);
|
||||
});
|
||||
|
||||
this.localClientMetadata = Array.from(existing.values());
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
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, kind) {
|
||||
const base = {
|
||||
source: kind,
|
||||
name: file?.name || '',
|
||||
size: file?.size || 0,
|
||||
captured_at: new Date(file?.lastModified || Date.now()).toISOString(),
|
||||
device: this.getDeviceLabel(),
|
||||
};
|
||||
|
||||
if (kind !== 'camera') {
|
||||
return base;
|
||||
}
|
||||
|
||||
const geo = await this.readGeolocation();
|
||||
|
||||
return geo ? { ...base, ...geo } : base;
|
||||
},
|
||||
async setPreviews(event, kind) {
|
||||
const target = kind === 'camera' ? 'localCameraPreviews' : 'localAttachmentPreviews';
|
||||
const batchId = Date.now();
|
||||
const mapped = Array.from(event?.target?.files || []).map((file, index) => ({
|
||||
const files = Array.from(event?.target?.files || []);
|
||||
const mapped = files.map((file, index) => ({
|
||||
key: `${kind}-${batchId}-${index}-${file.name}-${file.size}`,
|
||||
name: file.name,
|
||||
draftName: this.buildDraftName(file, this.draftSequence + index),
|
||||
|
|
@ -296,6 +377,7 @@
|
|||
url: (file.type || '').startsWith('image/') ? URL.createObjectURL(file) : null,
|
||||
}));
|
||||
this.appendPreviews(target, mapped);
|
||||
this.mergeClientMetadata(await Promise.all(files.map((file) => this.buildClientMetadata(file, kind))));
|
||||
this.persist();
|
||||
if (event?.target) {
|
||||
event.target.value = null;
|
||||
|
|
@ -305,12 +387,19 @@
|
|||
if (kind === null || kind === 'camera') {
|
||||
this.revoke(this.localCameraPreviews);
|
||||
this.localCameraPreviews = [];
|
||||
this.localClientMetadata = kind === null
|
||||
? []
|
||||
: this.localClientMetadata.filter((item) => item.source !== 'camera');
|
||||
}
|
||||
if (kind === null || kind === 'attachment') {
|
||||
this.revoke(this.localAttachmentPreviews);
|
||||
this.localAttachmentPreviews = [];
|
||||
if (kind !== null) {
|
||||
this.localClientMetadata = this.localClientMetadata.filter((item) => item.source !== 'attachment');
|
||||
}
|
||||
}
|
||||
this.persist();
|
||||
this.persistClientMetadata();
|
||||
},
|
||||
previewFor(draftName, originalName, size = 0) {
|
||||
const pool = [...this.localCameraPreviews, ...this.localAttachmentPreviews];
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user