From e6aff8f512a173ff61c7d80205683df6b7755fd6 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Fri, 12 Jun 2026 21:42:36 +0200 Subject: [PATCH] optimize scanner lpr frontend --- .../steps/mobile/PosDepartmentStepMobile1.vue | 696 +++++++++++-- ...DepartmentStepMobileFixedBottomControl.vue | 14 +- .../objects/PosDepartmentStepMobileFlow.vue | 29 +- .../global/RequestQueueProgress.vue | 205 +++- .../session/authenticatedRequest.vue | 125 ++- .../scanner/graphics/ScannerCamera.vue | 616 ++++++++++-- .../page/templates/scanner/lprFrameCapture.ts | 773 ++++++++++++++ src/services/installAxiosRequestQueue.js | 1 + src/services/requestQueue.js | 266 ++++- tests/unit/authenticated-request.spec.js | 318 +++++- tests/unit/lpr-frame-capture.spec.js | 891 ++++++++++++++++ tests/unit/pos-mobile-camera-lpr.spec.js | 948 +++++++++++++++++- tests/unit/request-queue-progress.spec.js | 254 ++++- .../scanner-camera-capture-enabled.spec.js | 883 ++++++++++++++++ 14 files changed, 5763 insertions(+), 256 deletions(-) create mode 100644 src/components/viewport/page/templates/scanner/lprFrameCapture.ts create mode 100644 tests/unit/lpr-frame-capture.spec.js create mode 100644 tests/unit/scanner-camera-capture-enabled.spec.js diff --git a/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue b/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue index 30be16f3..d6d68c55 100644 --- a/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue +++ b/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue @@ -24,71 +24,79 @@ import { } from "./objects/PosDepartmentStepMobileFlow.vue"; import { PosSearchResult } from "./objects/PosSearchResult.vue"; import RegistrationNumberSearchResult from "@/components/models/pos/step1/RegistrationNumberSearchResult.vue"; -import UnknownCustomer from "@/components/viewport/elements/icons/UnknownCustomer.vue"; -import VerifiedCustomer from "@/components/viewport/elements/icons/VerifiedCustomer.vue"; -import BookedCustomer from "@/components/viewport/elements/icons/BookedCustomer.vue"; -import KnownCustomer from "@/components/viewport/elements/icons/KnownCustomer.vue"; -import CardPaymentCustomer from "@/components/viewport/elements/icons/CardPaymentCustomer.vue"; import PosDepartmentStepMobileFixedBottomControl from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileFixedBottomControl.vue"; import PosDepartmentStepMobile1Location from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Location.vue"; import PosDepartmentStepMobile1Debug from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Debug.vue"; import PosDepartmentStepMobileAttachments from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileAttachments.vue"; import PosDepartmentStep1MobileTransactionHistory from "@/components/displays/department/pos/steps/mobile/views/PosDepartmentStep1MobileTransactionHistory.vue"; import { attachments } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue"; +import { + LPR_FRAME_CLIENT_BYTES_FIELD, + LPR_FRAME_CLIENT_CAPTURE_MS_FIELD, + LPR_FRAME_CLIENT_DRAW_MS_FIELD, + LPR_FRAME_CLIENT_ENCODE_MS_FIELD, + LPR_FRAME_CLIENT_HEIGHT_FIELD, + LPR_FRAME_CLIENT_PREFLIGHT_MS_FIELD, + LPR_FRAME_CLIENT_VISUAL_FINGERPRINT_MS_FIELD, + LPR_FRAME_CLIENT_WIDTH_FIELD, + getVisualFingerprintDistance, + type LPRFrameEncodeCandidate, + type LPRFramePayload, + type LPRFrameViewportRect, +} from "@/components/viewport/page/templates/scanner/lprFrameCapture"; // Debug mode flag const debug_mode = ref(false); // Debug array to store request results -const debug_request_results = ref([]); +const debug_request_results = ref([]); -const lastParsedImage = ref(null); -const setLastCapturedImage = (image: string) => { - camera.latestImage.value = image; +type ParsedFrameFingerprint = { + content: string | null; + contentFingerprintPromise: Promise | null; + getContentFingerprint: (() => Promise) | null; + getVisualFingerprint: (() => string | null) | null; + outcome: "pending" | "miss" | "success"; + quick: string; + visual: string | null; }; +const lastParsedImage = ref(null); +const scannerFocusRef = ref(null); + type LPRResponse = { success: boolean; license_plate_number: string; }; -const latestLPRResponse = ref(null); -const isLPRRequestInFlight = ref(false); -const LPR_IMAGE_MAX_WIDTH = 1280; -const LPR_IMAGE_MAX_HEIGHT = 720; -const LPR_IMAGE_JPEG_QUALITY = 0.72; - -const compressImageForLPR = (image: string): Promise => { - return new Promise((resolve) => { - const img = new Image(); - img.onload = () => { - const sourceWidth = img.naturalWidth || img.width; - const sourceHeight = img.naturalHeight || img.height; - if (!sourceWidth || !sourceHeight) { - resolve(image); - return; - } - - const scale = Math.min(1, LPR_IMAGE_MAX_WIDTH / sourceWidth, LPR_IMAGE_MAX_HEIGHT / sourceHeight); - const targetWidth = Math.max(1, Math.round(sourceWidth * scale)); - const targetHeight = Math.max(1, Math.round(sourceHeight * scale)); - - const canvas = document.createElement("canvas"); - canvas.width = targetWidth; - canvas.height = targetHeight; - - const context = canvas.getContext("2d"); - if (!context) { - resolve(image); - return; - } - - context.drawImage(img, 0, 0, targetWidth, targetHeight); - resolve(canvas.toDataURL("image/jpeg", LPR_IMAGE_JPEG_QUALITY)); - }; - img.onerror = () => resolve(image); - img.src = image; - }); +type LPRScanContext = { + activeVehicleIndex: number; + attachmentView: boolean; + manualInput: boolean; + registrationNumbers: string[]; + transactionHistoryView: boolean; }; +const latestLPRResponse = ref(null); +const isLPRFrameProcessing = ref(false); +const isLPRRequestInFlight = ref(false); +const isNoPlateBackoffActive = ref(false); +const isDuplicateFrameBackoffActive = ref(false); +const isSuccessCooldownActive = ref(false); +let lprRequestAbortController: AbortController | null = null; +let noPlateBackoffTimerId: ReturnType | null = null; +let duplicateFrameBackoffTimerId: ReturnType | null = null; +let successCooldownTimerId: ReturnType | null = null; + +const NO_PLATE_BACKOFF_DELAYS_MS = [1500, 2500, 4000]; +const LPR_VISUAL_DUPLICATE_DISTANCE_THRESHOLD = 4; +const LPR_VISUAL_DUPLICATE_RECHECK_DELAY_MS = 700; +const LPR_ENDPOINT = "/modules/scanner/lpr"; +let consecutiveNoPlateResponses = 0; + +const nowMs = (): number => + typeof performance !== "undefined" && typeof performance.now === "function" + ? performance.now() + : Date.now(); + const activeVehicleIndexNext = () => { // Increment the active vehicle index, wrapping around if necessary if (vehicles.activeVehicleIndex.value < 3) { @@ -116,48 +124,511 @@ const handleLPRResult = () => { } }; -const parseImage = async (image: string) => { - if (isLPRRequestInFlight.value) { +type LPRFrameInput = string | LPRFramePayload; + +const isLPRFramePayload = (image: LPRFrameInput): image is LPRFramePayload => + typeof image === "object" && image !== null && image.blob instanceof Blob; + +const getLPRFrameFingerprint = (image: LPRFrameInput): string => + isLPRFramePayload(image) ? image.fingerprint : image; + +const getLPRFrameContentFingerprint = (image: LPRFrameInput): (() => Promise) | null => + isLPRFramePayload(image) ? image.getContentFingerprint ?? null : null; + +const getLPRFrameVisualFingerprint = (image: LPRFrameInput): string | null => + isLPRFramePayload(image) ? image.visualFingerprint ?? null : null; + +const getLPRFrameVisualFingerprintGetter = (image: LPRFrameInput): (() => string | null) | null => + isLPRFramePayload(image) ? image.getVisualFingerprint ?? null : null; + +const rememberParsedImageFingerprint = (image: LPRFrameInput) => { + const quick = getLPRFrameFingerprint(image); + const getContentFingerprint = getLPRFrameContentFingerprint(image); + const entry: ParsedFrameFingerprint = { + content: getContentFingerprint === null ? quick : null, + contentFingerprintPromise: null, + getContentFingerprint, + getVisualFingerprint: getLPRFrameVisualFingerprintGetter(image), + outcome: "pending", + quick, + visual: getLPRFrameVisualFingerprint(image), + }; + + lastParsedImage.value = entry; +}; + +const markLastParsedImageOutcome = (outcome: ParsedFrameFingerprint["outcome"]) => { + if (lastParsedImage.value !== null) { + if (outcome === "miss" && lastParsedImage.value.visual === null) { + lastParsedImage.value.visual = lastParsedImage.value.getVisualFingerprint?.() ?? null; + } + + lastParsedImage.value.outcome = outcome; + } +}; + +const resetParsedImageFingerprint = () => { + lastParsedImage.value = null; +}; + +const resolveParsedFrameContentFingerprint = (entry: ParsedFrameFingerprint): Promise | null => { + if (entry.content !== null) { + return Promise.resolve(entry.content); + } + + if (entry.getContentFingerprint === null) { + return null; + } + + entry.contentFingerprintPromise ??= entry.getContentFingerprint().then((content) => { + if (lastParsedImage.value === entry) { + entry.content = content; + } + + return content; + }).catch((error) => { + if (lastParsedImage.value === entry) { + entry.contentFingerprintPromise = null; + } + + throw error; + }); + + return entry.contentFingerprintPromise; +}; + +const isVisuallySimilarToLastMiss = (visualFingerprint: string | null | undefined): boolean => { + const lastParsed = lastParsedImage.value; + + return lastParsed !== null + && lastParsed.outcome === "miss" + && getVisualFingerprintDistance(lastParsed.visual, visualFingerprint) <= LPR_VISUAL_DUPLICATE_DISTANCE_THRESHOLD; +}; + +const hasLastMissVisualFingerprint = (): boolean => + lastParsedImage.value !== null + && lastParsedImage.value.outcome === "miss" + && lastParsedImage.value.visual !== null; + +const shouldBuildLPRVisualFingerprint = (): boolean => + hasLastMissVisualFingerprint(); + +const shouldSkipDuplicateFrame = async (image: LPRFrameInput): Promise => { + const quick = getLPRFrameFingerprint(image); + const lastParsed = lastParsedImage.value; + + if (lastParsed === null) { + return false; + } + + if (isVisuallySimilarToLastMiss(getLPRFrameVisualFingerprint(image))) { + return true; + } + + if (lastParsed.quick !== quick) { + return false; + } + + const currentContentFingerprint = getLPRFrameContentFingerprint(image); + const lastContentFingerprint = resolveParsedFrameContentFingerprint(lastParsed); + if (lastContentFingerprint === null || currentContentFingerprint === null) { + return true; + } + + try { + const [lastContent, currentContent] = await Promise.all([ + lastContentFingerprint, + currentContentFingerprint(), + ]); + + return lastContent === currentContent; + } catch { + return false; + } +}; + +const appendFiniteTimingParam = ( + queryParts: string[], + field: string, + value: number | null | undefined +) => { + if (value === null || value === undefined) { + return; + } + + const numericValue = Number(value); + if (!Number.isFinite(numericValue) || numericValue < 0) { + return; + } + + const roundedValue = numericValue.toFixed(3); + if (Number(roundedValue) > 0) { + queryParts.push(`${field}=${roundedValue}`); + } +}; + +const appendPositiveIntegerParam = ( + queryParts: string[], + field: string, + value: number | null | undefined +) => { + const numericValue = Number(value); + if (!Number.isFinite(numericValue) || numericValue <= 0) { + return; + } + + queryParts.push(`${field}=${Math.round(numericValue)}`); +}; + +type LPRRequestPayload = Blob | { base64_image: string }; +type LPRRequestBuildResult = { + headers?: Record; + payload: LPRRequestPayload; + url: string; +}; + +const buildLPRRequestPayload = ( + image: LPRFrameInput, + clientPreflightDurationMs: number | null = null +): LPRRequestBuildResult => { + if (!isLPRFramePayload(image)) { + return { + payload: { base64_image: image }, + url: LPR_ENDPOINT, + }; + } + + const headers: Record = { + "Content-Type": image.mimeType || image.blob.type || "image/jpeg", + }; + const queryParts: string[] = []; + appendFiniteTimingParam(queryParts, LPR_FRAME_CLIENT_CAPTURE_MS_FIELD, image.captureDurationMs); + appendFiniteTimingParam(queryParts, LPR_FRAME_CLIENT_PREFLIGHT_MS_FIELD, clientPreflightDurationMs); + appendFiniteTimingParam(queryParts, LPR_FRAME_CLIENT_DRAW_MS_FIELD, image.captureTimings?.drawMs); + appendFiniteTimingParam(queryParts, LPR_FRAME_CLIENT_ENCODE_MS_FIELD, image.captureTimings?.encodeMs); + appendFiniteTimingParam( + queryParts, + LPR_FRAME_CLIENT_VISUAL_FINGERPRINT_MS_FIELD, + image.captureTimings?.visualFingerprintMs + ); + appendPositiveIntegerParam(queryParts, LPR_FRAME_CLIENT_WIDTH_FIELD, image.width); + appendPositiveIntegerParam(queryParts, LPR_FRAME_CLIENT_HEIGHT_FIELD, image.height); + appendPositiveIntegerParam(queryParts, LPR_FRAME_CLIENT_BYTES_FIELD, image.blob.size); + const queryString = queryParts.join("&"); + + return { + headers, + payload: image.blob, + url: queryString ? `${LPR_ENDPOINT}?${queryString}` : LPR_ENDPOINT, + }; +}; + +type LPRCurrentStateSkipOptions = { + ignoreNoPlateBackoff?: boolean; +}; + +const shouldEncodeLPRFrame = (candidate: LPRFrameEncodeCandidate): boolean => { + if (views.attachmentView.value) { + return true; + } + + if ( + shouldSkipLPRForCurrentState({ ignoreNoPlateBackoff: true }) + || isLPRFrameProcessing.value + || isLPRRequestInFlight.value + ) { + return false; + } + + if (isNoPlateBackoffActive.value) { + if (!hasLastMissVisualFingerprint() || isVisuallySimilarToLastMiss(candidate.visualFingerprint)) { + return false; + } + + clearNoPlateBackoff(); + return true; + } + + if (!isVisuallySimilarToLastMiss(candidate.visualFingerprint)) { + return true; + } + + scheduleDuplicateFrameBackoff(); + return false; +}; + +const rememberLatestCameraImage = (image: LPRFrameInput) => { + if (isLPRFramePayload(image)) { + camera.setLatestImageBlob(image.blob); + return; + } + + camera.setLatestImage(image); +}; + +const hasRegistrationNumber = (registrationNumber: string | null | undefined): boolean => + String(registrationNumber ?? "").trim().length > 0; + +const isActiveRegistrationSlotFilled = (): boolean => + hasRegistrationNumber(vehicles.getActiveVehicle()?.reg); + +const areAllRegistrationSlotsFilled = (): boolean => + [1, 2, 3].every((vehicleIndex) => hasRegistrationNumber(vehicles.get(vehicleIndex)?.reg)); + +const shouldSkipLPRForCurrentState = (options: LPRCurrentStateSkipOptions = {}): boolean => + views.attachmentView.value + || isActiveRegistrationSlotFilled() + || areAllRegistrationSlotsFilled() + || (!options.ignoreNoPlateBackoff && isNoPlateBackoffActive.value) + || isDuplicateFrameBackoffActive.value + || isSuccessCooldownActive.value; + +const getScannerFocusViewportRect = (): LPRFrameViewportRect | null => { + if (views.attachmentView.value || scannerFocusRef.value === null) { + return null; + } + + const rect = scannerFocusRef.value.getBoundingClientRect(); + if ( + !Number.isFinite(rect.width) || + !Number.isFinite(rect.height) || + rect.width <= 0 || + rect.height <= 0 + ) { + return null; + } + + return { + height: rect.height, + width: rect.width, + x: rect.left, + y: rect.top, + }; +}; + +const isCameraFrameCaptureEnabled = computed(() => { + if (views.attachmentView.value) { + return true; + } + + return !isLPRFrameProcessing.value + && !isLPRRequestInFlight.value + && (!isNoPlateBackoffActive.value || hasLastMissVisualFingerprint()) + && !isDuplicateFrameBackoffActive.value + && !isSuccessCooldownActive.value + && !isActiveRegistrationSlotFilled() + && !areAllRegistrationSlotsFilled(); +}); + +const shouldPauseScannerPreview = computed(() => + !views.attachmentView.value + && ( + isLPRFrameProcessing.value + || isLPRRequestInFlight.value + || isDuplicateFrameBackoffActive.value + || isSuccessCooldownActive.value + || isActiveRegistrationSlotFilled() + || areAllRegistrationSlotsFilled() + || (isNoPlateBackoffActive.value && !hasLastMissVisualFingerprint()) + ) +); + +const lprCameraCaptureIntervalMs = computed(() => + !views.attachmentView.value && isNoPlateBackoffActive.value && hasLastMissVisualFingerprint() + ? LPR_VISUAL_DUPLICATE_RECHECK_DELAY_MS + : null +); + +const abortLPRRequest = () => { + lprRequestAbortController?.abort(); + lprRequestAbortController = null; +}; + +const isDocumentHidden = (): boolean => + typeof document !== "undefined" && document.visibilityState === "hidden"; + +const handleDocumentVisibilityChange = () => { + if (!isDocumentHidden()) { + return; + } + + abortLPRRequest(); + resetParsedImageFingerprint(); + resetNoPlateBackoff(); +}; + +const clearNoPlateBackoff = () => { + if (noPlateBackoffTimerId !== null) { + window.clearTimeout(noPlateBackoffTimerId); + noPlateBackoffTimerId = null; + } + isNoPlateBackoffActive.value = false; +}; + +const clearDuplicateFrameBackoff = () => { + if (duplicateFrameBackoffTimerId !== null) { + window.clearTimeout(duplicateFrameBackoffTimerId); + duplicateFrameBackoffTimerId = null; + } + isDuplicateFrameBackoffActive.value = false; +}; + +const clearSuccessCooldown = () => { + if (successCooldownTimerId !== null) { + window.clearTimeout(successCooldownTimerId); + successCooldownTimerId = null; + } + isSuccessCooldownActive.value = false; +}; + +const resetNoPlateBackoff = () => { + consecutiveNoPlateResponses = 0; + clearNoPlateBackoff(); + clearDuplicateFrameBackoff(); + clearSuccessCooldown(); +}; + +const scheduleNoPlateBackoff = () => { + consecutiveNoPlateResponses += 1; + const delay = NO_PLATE_BACKOFF_DELAYS_MS[ + Math.min(consecutiveNoPlateResponses - 1, NO_PLATE_BACKOFF_DELAYS_MS.length - 1) + ]; + + clearNoPlateBackoff(); + isNoPlateBackoffActive.value = true; + noPlateBackoffTimerId = window.setTimeout(() => { + noPlateBackoffTimerId = null; + isNoPlateBackoffActive.value = false; + }, delay); +}; + +const scheduleDuplicateFrameBackoff = () => { + clearDuplicateFrameBackoff(); + isDuplicateFrameBackoffActive.value = true; + duplicateFrameBackoffTimerId = window.setTimeout(() => { + duplicateFrameBackoffTimerId = null; + isDuplicateFrameBackoffActive.value = false; + }, Math.min(camera.getImageCaptureDelay(false), LPR_VISUAL_DUPLICATE_RECHECK_DELAY_MS)); +}; + +const scheduleSuccessCooldown = () => { + clearSuccessCooldown(); + isSuccessCooldownActive.value = true; + successCooldownTimerId = window.setTimeout(() => { + successCooldownTimerId = null; + isSuccessCooldownActive.value = false; + }, camera.getImageCaptureDelayAfterSuccess()); +}; + +const isAbortError = (error: unknown): boolean => { + if (error instanceof DOMException && error.name === "AbortError") { + return true; + } + + return typeof error === "object" && error !== null && (error as { name?: string; code?: string }).code === "ERR_CANCELED"; +}; + +const isSameLPRScanContext = (first: LPRScanContext, second: LPRScanContext): boolean => + first.activeVehicleIndex === second.activeVehicleIndex + && first.attachmentView === second.attachmentView + && first.manualInput === second.manualInput + && first.transactionHistoryView === second.transactionHistoryView + && first.registrationNumbers.length === second.registrationNumbers.length + && first.registrationNumbers.every((registrationNumber, index) => registrationNumber === second.registrationNumbers[index]); + +const parseImage = async (image: LPRFrameInput) => { + if (views.attachmentView.value) { + rememberLatestCameraImage(image); + return; + } + + if (shouldSkipLPRForCurrentState() || isLPRFrameProcessing.value || isLPRRequestInFlight.value) { return; } // Check if the time since the last successful parse is enough if (!camera.hasDelayAfterSuccessPassed()) { return; } - if (lastParsedImage.value === image) { - // If the image is the same as the last parsed one, skip parsing - return; - } - lastParsedImage.value = image; // Update the last parsed image - camera.setLatestImage(image); // Update the latest image in the camera object - isLPRRequestInFlight.value = true; + isLPRFrameProcessing.value = true; try { - // Function to parse the image data - const compressedImage = await compressImageForLPR(image); - const response = await SessionUser.request("/modules/scanner/lpr", "POST", { - base64_image: compressedImage, - }); + const clientPreflightStartedAt = nowMs(); + const isDuplicateFrame = await shouldSkipDuplicateFrame(image); + const clientPreflightDurationMs = Math.max(0, nowMs() - clientPreflightStartedAt); - if (debug_mode.value) { - debug_request_results.value.push(response); - } - // If the response is not successful, stop here. - if (!response.data.success) { + if (shouldSkipLPRForCurrentState()) { return; } - latestLPRResponse.value = response.data.data as LPRResponse; - // Set the last successful capture time - camera.setLastSuccess(); - // Handle parsed result. - handleLPRResult(); - } catch (error) { - if (debug_mode.value) { - debug_request_results.value.push(error); + + if (isDuplicateFrame) { + // If the image is the same as the last parsed one, skip parsing + scheduleDuplicateFrameBackoff(); + return; + } + + rememberParsedImageFingerprint(image); // Update the last parsed image + isLPRRequestInFlight.value = true; + const abortController = new AbortController(); + lprRequestAbortController = abortController; + const requestScanContext = getLPRScanContext(); + const lprRequest = buildLPRRequestPayload(image, clientPreflightDurationMs); + + try { + const response = await SessionUser.request( + lprRequest.url, + "POST", + lprRequest.payload, + null, + null, + { + ...(lprRequest.headers ? { headers: lprRequest.headers } : {}), + signal: abortController.signal, + transport: "fetch", + } + ); + + if (debug_mode.value) { + debug_request_results.value.push(response); + } + + if (!isSameLPRScanContext(requestScanContext, getLPRScanContext())) { + return; + } + + // If the response is not successful, stop here. + if (!response.data.success) { + markLastParsedImageOutcome("miss"); + scheduleNoPlateBackoff(); + return; + } + resetNoPlateBackoff(); + markLastParsedImageOutcome("success"); + rememberLatestCameraImage(image); + latestLPRResponse.value = response.data.data as LPRResponse; + // Set the last successful capture time + camera.setLastSuccess(); + scheduleSuccessCooldown(); + // Handle parsed result. + handleLPRResult(); + } catch (error) { + if (isAbortError(error)) { + return; + } + + if (debug_mode.value) { + debug_request_results.value.push(error); + } + markLastParsedImageOutcome("miss"); + scheduleNoPlateBackoff(); + //console.error("Error parsing image:", error); + } finally { + if (lprRequestAbortController === abortController) { + lprRequestAbortController = null; + } + isLPRRequestInFlight.value = false; } - //console.error("Error parsing image:", error); } finally { - isLPRRequestInFlight.value = false; + isLPRFrameProcessing.value = false; } }; @@ -165,13 +636,6 @@ watch(manualInput, (newValue) => { // Update the header transparency when manualInput changes setTransparency(!newValue); }); -type statusIcon = - | typeof VerifiedCustomer - | typeof KnownCustomer - | typeof UnknownCustomer - | typeof CardPaymentCustomer - | typeof BookedCustomer; - const registrationNumbers = computed(() => { // Return the registration numbers of all vehicles return [ @@ -181,6 +645,14 @@ const registrationNumbers = computed(() => { ]; }); +const getLPRScanContext = (): LPRScanContext => ({ + activeVehicleIndex: vehicles.activeVehicleIndex.value, + attachmentView: views.attachmentView.value, + manualInput: manualInput.value, + registrationNumbers: [...registrationNumbers.value], + transactionHistoryView: transactionHistoryView.value, +}); + function getSearchResultIndex(object: PosSearchResult) { let targetIndex = vehicles.activeVehicleIndex.value; // Default to the current active vehicle index // Check if the registration number already exists in the vehicles (And update the vehicle if it does) @@ -236,27 +708,49 @@ const getQuery = computed(() => { return activeVehicle ? activeVehicle.reg : ""; }); -const activeSelectionHasRegistrationNumber = computed(() => { - return vehicles.getActiveVehicle()?.reg && vehicles.getActiveVehicle()?.reg.length > 0; -}); - const shouldCustomerBeModified = computed(() => { // Check if the customer should be modified based on the active vehicle index return vehicles.activeVehicleIndex.value === 1; }); onMounted(() => { + document.addEventListener("visibilitychange", handleDocumentVisibilityChange); // Set the header to be transparent setTransparency(!views.isAnyActive.value); setBackgroundColor(backgroundColors.default); // Set the default background color setOverflow(false); // Prevent scrolling }); onUnmounted(() => { + document.removeEventListener("visibilitychange", handleDocumentVisibilityChange); + abortLPRRequest(); + clearNoPlateBackoff(); + clearDuplicateFrameBackoff(); + clearSuccessCooldown(); // Reset the header settings when the component is unmounted setTransparency(false); // Reset transparency setBackgroundColor(backgroundColors.default); // Reset to default background color setOverflow(true); // Allow scrolling again }); + +watch( + () => [manualInput.value, transactionHistoryView.value, views.attachmentView.value], + ([isManualInputActive, isTransactionHistoryActive, isAttachmentViewActive]) => { + if (isManualInputActive || isTransactionHistoryActive || isAttachmentViewActive) { + abortLPRRequest(); + resetParsedImageFingerprint(); + resetNoPlateBackoff(); + } + } +); + +watch( + () => [vehicles.activeVehicleIndex.value, ...registrationNumbers.value], + () => { + abortLPRRequest(); + resetParsedImageFingerprint(); + resetNoPlateBackoff(); + } +);