Add scanner utility, modal, and stock scan page implementation
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
import {
|
||||
applyItemUpsert,
|
||||
getStockEntry,
|
||||
lookupItemByIdentifier,
|
||||
previewItemUpsert,
|
||||
searchItemDefinitions,
|
||||
} from '../../api/stock.js';
|
||||
import { BrowserMultiFormatReader } from '@zxing/browser';
|
||||
import { mapLookupItemToForm } from './identifier-lookup-mapper.js';
|
||||
import { fetchLocations } from '../../api/locations.js';
|
||||
import {
|
||||
@@ -15,6 +15,16 @@ import {
|
||||
import { STORAGE_KEYS } from '../../app/config.js';
|
||||
import { debounce, normalizeValidationError } from '../shared/form-utils.js';
|
||||
import { loadStoredValue, saveStoredValue } from '../shared/storage.js';
|
||||
import { renderScannerModal } from '../shared/scanner-modal.js';
|
||||
import {
|
||||
canUseCameraScanner,
|
||||
createScannerReader,
|
||||
normalizeIdentifierCode,
|
||||
parseKitchenScanPayload,
|
||||
normalizeScannerError,
|
||||
startCameraScanner,
|
||||
stopCameraScanner,
|
||||
} from '../shared/scanner.js';
|
||||
import { createAsyncState, runAsyncState } from '../shared/ui-state.js';
|
||||
|
||||
const STOCK_TYPE_OPTIONS = [
|
||||
@@ -35,8 +45,41 @@ const STOCK_LEVEL_OPTIONS = [
|
||||
const QUANTITY_UNIT_OPTIONS = ['g', 'ml', 'pc'];
|
||||
const EXPIRATION_DAY_OPTIONS = ['3', '5', '8', '10', '15', '20', '25', '30', '45', '60', '90', '120', '150', '180'];
|
||||
const LABEL_DRAFT_STALE_MS = 30 * 60 * 1000;
|
||||
const SCANNER_ACTION_OPTIONS = [
|
||||
{
|
||||
key: 'lookup',
|
||||
label: 'Lookup',
|
||||
description: 'Lookup identifier data and prefill the label form.',
|
||||
},
|
||||
{
|
||||
key: 'create',
|
||||
label: 'Create',
|
||||
description: 'Lookup data and create label immediately when required fields are complete.',
|
||||
},
|
||||
{
|
||||
key: 'create_print',
|
||||
label: 'Create & print',
|
||||
description: 'Lookup data, create label, and print when required fields are complete.',
|
||||
},
|
||||
];
|
||||
|
||||
export function renderLabelCreatePage() {
|
||||
const scannerOptionsMarkup = `
|
||||
<div class="scan-label-mode-list mb-3">
|
||||
<template x-for="action in scannerActionOptions" :key="'label-scanner-' + action.key">
|
||||
<button
|
||||
class="btn btn-sm scan-label-mode-btn"
|
||||
type="button"
|
||||
:class="scannerAction === action.key ? 'scan-label-mode-btn-active' : 'btn-outline-secondary'"
|
||||
@click="scannerAction = action.key"
|
||||
>
|
||||
<span x-text="action.label"></span>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="small text-body-secondary mb-3" x-text="activeScannerActionDescription()"></div>
|
||||
`;
|
||||
|
||||
return `
|
||||
<section class="container-xxl py-4 py-lg-5" x-data="labelCreatePage()" x-init="init()">
|
||||
<div class="d-flex flex-column flex-lg-row justify-content-between gap-3 align-items-lg-end mb-4">
|
||||
@@ -121,7 +164,7 @@ export function renderLabelCreatePage() {
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
Optional. Scan with camera or enter manually.
|
||||
Optional. Scan with camera, use a hardware scanner, or enter manually.
|
||||
</div>
|
||||
<template x-if="lookupState.error">
|
||||
<div class="small text-danger mt-1" x-text="lookupState.error"></div>
|
||||
@@ -468,7 +511,19 @@ export function renderLabelCreatePage() {
|
||||
</template>
|
||||
|
||||
<template x-if="printIssue">
|
||||
<div class="alert alert-warning mb-0 py-2" x-text="printIssue"></div>
|
||||
<div class="alert alert-warning mb-0 py-2 d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||
<span x-text="printIssue"></span>
|
||||
<button
|
||||
class="btn btn-sm btn-outline-secondary"
|
||||
type="button"
|
||||
x-show="canRetryLastLabelPrint()"
|
||||
@click="retryLastLabelPrint()"
|
||||
:disabled="printState.isLoading"
|
||||
>
|
||||
<span x-show="!printState.isLoading">Reprint label</span>
|
||||
<span x-show="printState.isLoading">Printing...</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 label-actions-row">
|
||||
@@ -522,38 +577,16 @@ export function renderLabelCreatePage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="scanner-modal-backdrop"
|
||||
x-show="scannerState.isOpen"
|
||||
@click.self="closeScanner()"
|
||||
@keydown.escape.window="closeScanner()"
|
||||
>
|
||||
<div class="scanner-modal card border-0 shadow-lg">
|
||||
<div class="card-body p-3 p-md-4">
|
||||
<div class="d-flex align-items-center justify-content-between gap-3 mb-3">
|
||||
<div>
|
||||
<h2 class="h5 mb-1">Scan barcode</h2>
|
||||
<p class="text-body-secondary small mb-0">Point your camera at the barcode to fill the identifier field.</p>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" @click="closeScanner()">Close</button>
|
||||
</div>
|
||||
|
||||
<div class="scanner-video-shell mb-3">
|
||||
<video class="scanner-video" x-ref="scannerVideo" autoplay muted playsinline></video>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||
<div class="small text-body-secondary" x-show="scannerState.isLoading">Starting camera...</div>
|
||||
<div class="small text-success" x-show="scannerState.lastDetectedCode" x-text="'Detected: ' + scannerState.lastDetectedCode"></div>
|
||||
<button class="btn btn-outline-secondary btn-sm" type="button" @click="startScanner()" :disabled="scannerState.isLoading">Retry</button>
|
||||
</div>
|
||||
|
||||
<template x-if="scannerState.error">
|
||||
<div class="alert alert-warning py-2 mt-3 mb-0" x-text="scannerState.error"></div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${renderScannerModal({
|
||||
title: 'Scan barcode',
|
||||
subtitle: 'Point your camera at a product barcode or kitchen DataMatrix label.',
|
||||
optionsMarkup: scannerOptionsMarkup,
|
||||
manualCodeModel: 'scannerManualCode',
|
||||
manualSubmitAction: 'processScannerManualCode()',
|
||||
manualPlaceholder: 'Scan with hardware reader or paste code',
|
||||
manualHelp: 'Use this with keyboard-style barcode scanners or for manual paste.',
|
||||
manualDisabledExpression: 'lookupState.isLoading || createState.isLoading || scannerState.isLoading || scannerState.isProcessing',
|
||||
})}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
@@ -677,6 +710,10 @@ export function labelCreatePageData(store) {
|
||||
previewState: createAsyncState(),
|
||||
createState: createAsyncState(),
|
||||
lookupState: createAsyncState(),
|
||||
printState: createAsyncState(),
|
||||
scannerActionOptions: SCANNER_ACTION_OPTIONS,
|
||||
scannerAction: 'lookup',
|
||||
scannerManualCode: '',
|
||||
stockTypeOptions: STOCK_TYPE_OPTIONS,
|
||||
stockLevelOptions: STOCK_LEVEL_OPTIONS,
|
||||
quantityUnitOptions: QUANTITY_UNIT_OPTIONS,
|
||||
@@ -695,11 +732,14 @@ export function labelCreatePageData(store) {
|
||||
upsertPreview: null,
|
||||
printLabelOnSave: true,
|
||||
printIssue: '',
|
||||
lastCreatedLabelUuidB64: '',
|
||||
lastCreatedLabelName: '',
|
||||
scannerReader: null,
|
||||
scannerControls: null,
|
||||
scannerState: {
|
||||
isOpen: false,
|
||||
isLoading: false,
|
||||
isProcessing: false,
|
||||
hasCamera: false,
|
||||
error: '',
|
||||
lastDetectedCode: '',
|
||||
@@ -746,14 +786,10 @@ export function labelCreatePageData(store) {
|
||||
this.stopScanner();
|
||||
},
|
||||
canUseCameraScanner() {
|
||||
return Boolean(
|
||||
typeof navigator !== 'undefined'
|
||||
&& navigator.mediaDevices
|
||||
&& typeof navigator.mediaDevices.getUserMedia === 'function',
|
||||
);
|
||||
return canUseCameraScanner();
|
||||
},
|
||||
normalizeIdentifierCode(value) {
|
||||
return String(value || '').replace(/\s+/g, '').trim();
|
||||
return normalizeIdentifierCode(value);
|
||||
},
|
||||
hasLookupIdentifierCode() {
|
||||
return Boolean(this.normalizeIdentifierCode(this.form.identifierCode));
|
||||
@@ -835,26 +871,16 @@ export function labelCreatePageData(store) {
|
||||
return `${parts.join(' ')}.`;
|
||||
},
|
||||
normalizeScannerError(error) {
|
||||
const message = String(error?.message || '');
|
||||
const normalized = message.toLowerCase();
|
||||
|
||||
if (error?.name === 'NotAllowedError' || normalized.includes('permission')) {
|
||||
return 'Camera access was denied. Allow access to scan, or enter the code manually.';
|
||||
}
|
||||
|
||||
if (error?.name === 'NotFoundError' || normalized.includes('requested device not found')) {
|
||||
return 'No camera was found on this device. Enter the identifier code manually.';
|
||||
}
|
||||
|
||||
if (error?.name === 'NotReadableError' || normalized.includes('could not start video source')) {
|
||||
return 'Camera is busy in another app. Close it there and try scanning again.';
|
||||
}
|
||||
|
||||
return 'Could not start barcode scanning. Enter the identifier code manually.';
|
||||
return normalizeScannerError(error);
|
||||
},
|
||||
activeScannerActionDescription() {
|
||||
return this.scannerActionOptions.find((action) => action.key === this.scannerAction)?.description || '';
|
||||
},
|
||||
async openScanner() {
|
||||
this.scannerState.error = '';
|
||||
this.scannerState.lastDetectedCode = '';
|
||||
this.scannerState.isProcessing = false;
|
||||
this.scannerManualCode = this.form.identifierCode || '';
|
||||
this.scannerState.isOpen = true;
|
||||
await this.$nextTick();
|
||||
await this.startScanner();
|
||||
@@ -877,45 +903,19 @@ export function labelCreatePageData(store) {
|
||||
|
||||
this.stopScanner();
|
||||
this.scannerState.isLoading = true;
|
||||
const shouldLogDecodeErrors = import.meta.env.DEV;
|
||||
let lastDecodeErrorName = '';
|
||||
let lastDecodeErrorAt = 0;
|
||||
|
||||
try {
|
||||
if (!this.scannerReader) {
|
||||
this.scannerReader = new BrowserMultiFormatReader();
|
||||
this.scannerReader = createScannerReader();
|
||||
}
|
||||
|
||||
this.scannerControls = await this.scannerReader.decodeFromConstraints(
|
||||
{
|
||||
audio: false,
|
||||
video: {
|
||||
facingMode: { ideal: 'environment' },
|
||||
},
|
||||
},
|
||||
const session = await startCameraScanner({
|
||||
reader: this.scannerReader,
|
||||
videoElement,
|
||||
(result, error) => {
|
||||
if (result) {
|
||||
this.onBarcodeDetected(result.getText?.() || '');
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// Continuous decode emits expected per-frame misses/errors before a valid barcode is found.
|
||||
// Keep the modal quiet and only surface startup failures from the outer catch block.
|
||||
if (shouldLogDecodeErrors) {
|
||||
const errorName = String(error?.name || 'UnknownError');
|
||||
const now = Date.now();
|
||||
if (errorName !== lastDecodeErrorName || now - lastDecodeErrorAt > 2000) {
|
||||
console.debug('[scanner] Ignoring frame decode error while scanning:', errorName, error?.message || '');
|
||||
lastDecodeErrorName = errorName;
|
||||
lastDecodeErrorAt = now;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
},
|
||||
);
|
||||
onDetected: (code) => this.onBarcodeDetected(code),
|
||||
});
|
||||
this.scannerReader = session.reader;
|
||||
this.scannerControls = session.controls;
|
||||
} catch (error) {
|
||||
this.scannerState.error = this.normalizeScannerError(error);
|
||||
} finally {
|
||||
@@ -923,27 +923,12 @@ export function labelCreatePageData(store) {
|
||||
}
|
||||
},
|
||||
stopScanner() {
|
||||
try {
|
||||
this.scannerControls?.stop?.();
|
||||
} catch {
|
||||
// Ignore cleanup errors when scanner is already stopped.
|
||||
}
|
||||
stopCameraScanner({
|
||||
reader: this.scannerReader,
|
||||
controls: this.scannerControls,
|
||||
videoElement: this.$refs.scannerVideo,
|
||||
});
|
||||
this.scannerControls = null;
|
||||
|
||||
try {
|
||||
this.scannerReader?.reset?.();
|
||||
} catch {
|
||||
// Ignore cleanup errors from stale reader state.
|
||||
}
|
||||
|
||||
const videoElement = this.$refs.scannerVideo;
|
||||
const stream = videoElement?.srcObject;
|
||||
if (stream && typeof stream.getTracks === 'function') {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
if (videoElement) {
|
||||
videoElement.srcObject = null;
|
||||
}
|
||||
},
|
||||
closeScanner() {
|
||||
this.stopScanner();
|
||||
@@ -951,18 +936,293 @@ export function labelCreatePageData(store) {
|
||||
this.scannerState.isLoading = false;
|
||||
this.scannerState.error = '';
|
||||
},
|
||||
onBarcodeDetected(rawCode) {
|
||||
const code = this.normalizeIdentifierCode(rawCode);
|
||||
if (!code || !this.scannerState.isOpen) {
|
||||
async resolveIdentifierCodeFromScan(rawCode) {
|
||||
const parsed = parseKitchenScanPayload(rawCode);
|
||||
if (parsed.type === 'empty' || parsed.type === 'unknown') {
|
||||
return {
|
||||
identifierCode: '',
|
||||
message: 'Scanned code could not be interpreted.',
|
||||
level: 'warning',
|
||||
};
|
||||
}
|
||||
|
||||
if (parsed.type === 'item') {
|
||||
const item = await getStockEntry(store, parsed.uuidB64, { allowInactive: true });
|
||||
const itemIdentifierCode = this.normalizeIdentifierCode(item?.identifier_code);
|
||||
if (!itemIdentifierCode) {
|
||||
return {
|
||||
identifierCode: '',
|
||||
message: `${item?.name || 'Scanned item'} has no identifier code. Add one first, then scan again.`,
|
||||
level: 'info',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
identifierCode: itemIdentifierCode,
|
||||
message: `Resolved identifier ${itemIdentifierCode} from ${item?.name || 'scanned item'}.`,
|
||||
level: 'success',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
identifierCode: this.normalizeIdentifierCode(parsed.identifierCode),
|
||||
message: '',
|
||||
level: 'success',
|
||||
};
|
||||
},
|
||||
applyLookupResult(response, identifierCode, { announceSuccess = true } = {}) {
|
||||
if (response.status !== 'ok') {
|
||||
const message = this.lookupStatusMessageWithDetails(response, identifierCode);
|
||||
this.lookupState.error = message;
|
||||
store.addAlert({
|
||||
type: response.status === 'not_found' ? 'info' : 'warning',
|
||||
message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!response.item || typeof response.item !== 'object') {
|
||||
const message = 'Lookup returned no item payload to apply.';
|
||||
this.lookupState.error = message;
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
const mapped = mapLookupItemToForm({
|
||||
form: this.form,
|
||||
lookupItem: response.item,
|
||||
locations: this.locations,
|
||||
});
|
||||
|
||||
if (!mapped.didUpdate) {
|
||||
const message = 'Lookup finished, but no compatible fields were returned.';
|
||||
this.lookupState.error = message;
|
||||
store.addAlert({
|
||||
type: 'info',
|
||||
message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
this.form = {
|
||||
...mapped.form,
|
||||
itemId: '',
|
||||
itemUuidB64: '',
|
||||
};
|
||||
if (mapped.locationSearch !== null) {
|
||||
this.locationSearch = mapped.locationSearch;
|
||||
}
|
||||
this.syncStockTypeState(this.form.stockType);
|
||||
this.syncStockTypeSelect();
|
||||
this.syncStockLevelSelect();
|
||||
this.syncLocationValidity();
|
||||
this.upsertPreview = null;
|
||||
this.previewState.error = '';
|
||||
this.submitError = '';
|
||||
if (this.previewUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
}
|
||||
this.previewUrl = '';
|
||||
this.suggestions = [];
|
||||
this.persistDraft();
|
||||
|
||||
if (announceSuccess) {
|
||||
store.addAlert({
|
||||
type: 'success',
|
||||
message: this.lookupSuccessMessage(response),
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
async lookupIdentifierDetailsByCode(identifierCode, { announceSuccess = true } = {}) {
|
||||
const normalizedCode = this.normalizeIdentifierCode(identifierCode);
|
||||
this.form.identifierCode = normalizedCode;
|
||||
this.lookupState.error = '';
|
||||
|
||||
if (!normalizedCode) {
|
||||
this.lookupState.error = 'Provide an identifier code before lookup.';
|
||||
return false;
|
||||
}
|
||||
|
||||
const response = await lookupItemByIdentifier(store, normalizedCode);
|
||||
return this.applyLookupResult(response, normalizedCode, { announceSuccess });
|
||||
},
|
||||
canAutoCreateFromForm() {
|
||||
if (!String(this.form.name || '').trim()) {
|
||||
return false;
|
||||
}
|
||||
if (!this.form.productionDate) {
|
||||
return false;
|
||||
}
|
||||
if (!this.selectedLocation?.uuid_b64) {
|
||||
return false;
|
||||
}
|
||||
if (this.form.stockType === 'measured') {
|
||||
const quantity = Number(this.form.quantity);
|
||||
if (Number.isNaN(quantity) || quantity <= 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
async createFromScannerAction({ shouldPrint = false } = {}) {
|
||||
const entry = await applyItemUpsert(store, this.buildUpsertPayload());
|
||||
const entryName = entry.item?.name || this.form.name;
|
||||
const operationVerb = entry.operation === 'update' ? 'updated' : 'created';
|
||||
const createdUuidB64 = entry.item?.uuid_b64 || null;
|
||||
this.lastCreatedLabelUuidB64 = createdUuidB64 || '';
|
||||
this.lastCreatedLabelName = entryName;
|
||||
|
||||
this.successMessage = `${entryName} was ${operationVerb} successfully.`;
|
||||
this.upsertPreview = entry;
|
||||
saveLabelDraft(this.form);
|
||||
|
||||
if (shouldPrint) {
|
||||
if (!createdUuidB64) {
|
||||
const message = `${entryName} was ${operationVerb}, but label printing is unavailable for this entry.`;
|
||||
this.printIssue = message;
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
message,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
await printItemLabel(store, createdUuidB64);
|
||||
const message = `${entryName} was ${operationVerb} and printed. Ready for next scan.`;
|
||||
store.addAlert({
|
||||
type: 'success',
|
||||
message,
|
||||
});
|
||||
void this.openScanner().catch(() => {});
|
||||
} catch (printError) {
|
||||
const parsedPrintMessage = formatPrintErrorMessage(printError);
|
||||
this.printIssue = `${entryName} was ${operationVerb}, but printing failed: ${parsedPrintMessage}`;
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
message: this.printIssue,
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const message = `${entryName} was ${operationVerb}. Ready for next scan.`;
|
||||
store.addAlert({
|
||||
type: 'success',
|
||||
message,
|
||||
});
|
||||
void this.openScanner().catch(() => {});
|
||||
return true;
|
||||
},
|
||||
canRetryLastLabelPrint() {
|
||||
return Boolean(this.lastCreatedLabelUuidB64);
|
||||
},
|
||||
async retryLastLabelPrint() {
|
||||
if (!this.canRetryLastLabelPrint()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.identifierCode = code;
|
||||
await runAsyncState(this.printState, async () => {
|
||||
await printItemLabel(store, this.lastCreatedLabelUuidB64);
|
||||
this.printIssue = '';
|
||||
store.addAlert({
|
||||
type: 'success',
|
||||
message: `${this.lastCreatedLabelName || 'Label'} sent to printer.`,
|
||||
});
|
||||
}).catch((printError) => {
|
||||
const parsedPrintMessage = formatPrintErrorMessage(printError);
|
||||
const itemName = this.lastCreatedLabelName || 'Label';
|
||||
this.printIssue = `Could not print ${itemName}: ${parsedPrintMessage}`;
|
||||
});
|
||||
},
|
||||
async runScannerActionForCode(rawCode) {
|
||||
try {
|
||||
const resolved = await this.resolveIdentifierCodeFromScan(rawCode);
|
||||
if (!resolved.identifierCode) {
|
||||
store.addAlert({
|
||||
type: resolved.level || 'warning',
|
||||
message: resolved.message || 'Scanned code could not be used for lookup.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form.identifierCode = resolved.identifierCode;
|
||||
if (resolved.message) {
|
||||
store.addAlert({
|
||||
type: resolved.level || 'info',
|
||||
message: resolved.message,
|
||||
});
|
||||
}
|
||||
|
||||
const didLookupApply = await this.lookupIdentifierDetailsByCode(resolved.identifierCode, {
|
||||
announceSuccess: this.scannerAction === 'lookup',
|
||||
});
|
||||
if (!didLookupApply) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.scannerAction === 'lookup') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.canAutoCreateFromForm()) {
|
||||
const message = 'Lookup filled part of the form. Complete required fields, then save or print.';
|
||||
this.lookupState.error = message;
|
||||
store.addAlert({
|
||||
type: 'info',
|
||||
message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.submitError = '';
|
||||
this.fieldErrors = {};
|
||||
this.printIssue = '';
|
||||
await runAsyncState(this.createState, async () => {
|
||||
await this.createFromScannerAction({
|
||||
shouldPrint: this.scannerAction === 'create_print',
|
||||
});
|
||||
}).catch((error) => {
|
||||
this.fieldErrors = normalizeValidationError(error);
|
||||
this.submitError = error.message || 'Could not create from scanned lookup.';
|
||||
});
|
||||
} catch (error) {
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
message: `Scanner action failed: ${error.message || 'Unknown error.'}`,
|
||||
});
|
||||
}
|
||||
},
|
||||
processScannerManualCode() {
|
||||
const code = this.normalizeIdentifierCode(this.scannerManualCode);
|
||||
if (!code) {
|
||||
this.scannerState.error = 'Scan or enter a code first.';
|
||||
return;
|
||||
}
|
||||
this.scannerState.lastDetectedCode = code;
|
||||
this.scannerState.isProcessing = true;
|
||||
this.closeScanner();
|
||||
store.addAlert({
|
||||
type: 'success',
|
||||
message: `Scanned identifier code: ${code}`,
|
||||
this.runScannerActionForCode(code).finally(() => {
|
||||
this.scannerState.isProcessing = false;
|
||||
});
|
||||
},
|
||||
onBarcodeDetected(rawCode) {
|
||||
const code = this.normalizeIdentifierCode(rawCode);
|
||||
if (!code || !this.scannerState.isOpen || this.scannerState.isProcessing) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.scannerState.lastDetectedCode = code;
|
||||
this.scannerManualCode = code;
|
||||
this.scannerState.isProcessing = true;
|
||||
this.closeScanner();
|
||||
this.runScannerActionForCode(rawCode).finally(() => {
|
||||
this.scannerState.isProcessing = false;
|
||||
});
|
||||
},
|
||||
async lookupIdentifierDetails() {
|
||||
@@ -976,69 +1236,7 @@ export function labelCreatePageData(store) {
|
||||
}
|
||||
|
||||
await runAsyncState(this.lookupState, async () => {
|
||||
const response = await lookupItemByIdentifier(store, identifierCode);
|
||||
if (response.status !== 'ok') {
|
||||
const message = this.lookupStatusMessageWithDetails(response, identifierCode);
|
||||
this.lookupState.error = message;
|
||||
store.addAlert({
|
||||
type: response.status === 'not_found' ? 'info' : 'warning',
|
||||
message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.item || typeof response.item !== 'object') {
|
||||
const message = 'Lookup returned no item payload to apply.';
|
||||
this.lookupState.error = message;
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mapped = mapLookupItemToForm({
|
||||
form: this.form,
|
||||
lookupItem: response.item,
|
||||
locations: this.locations,
|
||||
});
|
||||
|
||||
if (!mapped.didUpdate) {
|
||||
const message = 'Lookup finished, but no compatible fields were returned.';
|
||||
this.lookupState.error = message;
|
||||
store.addAlert({
|
||||
type: 'info',
|
||||
message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.form = {
|
||||
...mapped.form,
|
||||
itemId: '',
|
||||
itemUuidB64: '',
|
||||
};
|
||||
if (mapped.locationSearch !== null) {
|
||||
this.locationSearch = mapped.locationSearch;
|
||||
}
|
||||
this.syncStockTypeState(this.form.stockType);
|
||||
this.syncStockTypeSelect();
|
||||
this.syncStockLevelSelect();
|
||||
this.syncLocationValidity();
|
||||
this.upsertPreview = null;
|
||||
this.previewState.error = '';
|
||||
this.submitError = '';
|
||||
if (this.previewUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
}
|
||||
this.previewUrl = '';
|
||||
this.suggestions = [];
|
||||
this.persistDraft();
|
||||
|
||||
store.addAlert({
|
||||
type: 'success',
|
||||
message: this.lookupSuccessMessage(response),
|
||||
});
|
||||
await this.lookupIdentifierDetailsByCode(identifierCode, { announceSuccess: true });
|
||||
}).catch((error) => {
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
@@ -1501,16 +1699,18 @@ export function labelCreatePageData(store) {
|
||||
const entryName = entry.item?.name || this.form.name;
|
||||
const operationVerb = entry.operation === 'update' ? 'updated' : 'created';
|
||||
const createdUuidB64 = entry.item?.uuid_b64 || null;
|
||||
this.lastCreatedLabelUuidB64 = createdUuidB64 || '';
|
||||
this.lastCreatedLabelName = entryName;
|
||||
|
||||
if (this.printLabelOnSave && createdUuidB64) {
|
||||
try {
|
||||
await printItemLabel(store, createdUuidB64);
|
||||
} catch (printError) {
|
||||
const parsedPrintMessage = formatPrintErrorMessage(printError);
|
||||
this.printIssue = parsedPrintMessage;
|
||||
this.printIssue = `${entryName} was ${operationVerb}, but printing failed: ${parsedPrintMessage}`;
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
message: `${entryName} was ${operationVerb}, but printing has an issue: ${parsedPrintMessage}`,
|
||||
message: this.printIssue,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1542,6 +1742,8 @@ export function labelCreatePageData(store) {
|
||||
this.fieldErrors = {};
|
||||
this.upsertPreview = null;
|
||||
this.printIssue = '';
|
||||
this.lastCreatedLabelUuidB64 = '';
|
||||
this.lastCreatedLabelName = '';
|
||||
saveLabelDraft(this.form);
|
||||
if (revokePreview && this.previewUrl.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(this.previewUrl);
|
||||
|
||||
Reference in New Issue
Block a user