Add BarcodeScanner.vue and VerifiedCustomer.vue icons, enhance POS mobile workflow:

- Introduce `BarcodeScanner.vue` and `VerifiedCustomer.vue` for improved visual elements in the POS module.
- Implement manual registration input flow in `PosDepartmentStepMobile1.vue` with `PosDepartmentStep1MobileManualInput.vue` for enhanced user flexibility.
- Add responsive button width adjustments in `GenericButton.vue` for consistent styling.
- Refine `ScannerCamera.vue` frame capture logic and improve header background integration with `ViewportHeaderSettings.vue`.
- Ensure dynamic background color via `Viewport.vue`.
This commit is contained in:
Jeppe Bundgaard
2025-08-06 15:16:22 +02:00
parent cdbca11f39
commit 9dcbfe298a
10 changed files with 505 additions and 41 deletions
@@ -4,21 +4,32 @@ import ScannerCamera from "@/components/viewport/page/templates/scanner/graphics
import ScannerInstructions from "@/components/viewport/page/templates/scanner/graphics/ScannerInstructions.vue";
import ScannerOutline from "@/components/viewport/page/templates/scanner/graphics/ScannerOutline.vue";
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
import { setTransparency } from "@/components/viewport/page/headers/ViewportHeaderSettings.vue";
import { onMounted } from "vue";
import { setTransparency, setBackgroundColor, backgroundColors } from "@/components/viewport/page/headers/ViewportHeaderSettings.vue";
import { onMounted, ref, watch } from "vue";
import PosDepartmentStepMobile1RegistrationNumbers
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1RegistrationNumbers.vue";
import ReaderTest from "@/assets/test/ReaderTest.vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import PosDepartmentStep1MobileManualInput
from "@/components/displays/department/pos/steps/mobile/views/PosDepartmentStep1MobileManualInput.vue";
const manualInput = ref(true);
onMounted(() => {
// Set the header to be transparent
setTransparency(true);
setTransparency(!manualInput.value); // Set transparency based on manualInput state
setBackgroundColor(backgroundColors.default); // Set the default background color
});
const lastParsedImage = ref(null);
const parseImage = (image: string) => {
if (lastParsedImage.value === image) {
// If the image is the same as the last parsed one, skip parsing
console.log("Skipping parsing for the same image.");
return;
}
lastParsedImage.value = image; // Update the last parsed image
// Function to parse the image data
console.log("Parsing image:", image);
// Add your parsing logic here
SessionUser.request(
"/modules/scanner/lpr",
"POST",
@@ -27,36 +38,46 @@ const parseImage = (image: string) => {
},
)
};
watch(manualInput, (newValue) => {
// Update the header transparency when manualInput changes
setTransparency(!newValue);
});
</script>
<template>
<div class="background-fixed">
<ScannerCamera
@update:frame="parseImage"
<template v-if="manualInput">
<PosDepartmentStep1MobileManualInput @close="manualInput = false"/>
</template>
<template v-else>
<div class="background-fixed">
<ScannerCamera
@update:frame="parseImage"
/>
</div>
<!--<ReaderTest/>-->
<!--<LetterReader/>-->
<!-- Instructions for the user -->
<ScannerInstructions
title="Scan Plates"
subtitle="Position the camera in front of the plates."
buttonText="Type registration number manually"
@button-click="manualInput = true"
/>
</div>
<!--<ReaderTest/>-->
<!--<LetterReader/>-->
<!-- Instructions for the user -->
<ScannerInstructions
title="Scan Plates"
subtitle="Position the camera in front of the plates."
buttonText="Type registration number manually"
@button-click="console.log('Button clicked')"
/>
<!-- Scanner outline object -->
<div class="is-align-content-center is-flex is-justify-content-center mt-6">
<ScannerOutline :loading="false"/>
</div>
<!-- Reg. 1, Reg. 2, Reg. 3 -->
<div class="mt-6 is-flex is-align-content-center is-justify-content-center is-flex-direction-column">
<PosDepartmentStepMobile1RegistrationNumbers/>
</div>
<!-- Buttons -->
<div class="is-flex is-justify-content-center mt-6">
<GenericButton class="white">Next</GenericButton>
</div>
<!-- Additional content or components can be added here -->
<!-- Scanner outline object -->
<div class="is-align-content-center is-flex is-justify-content-center mt-6">
<ScannerOutline :loading="false"/>
</div>
<!-- Reg. 1, Reg. 2, Reg. 3 -->
<div class="mt-6 is-flex is-align-content-center is-justify-content-center is-flex-direction-column">
<PosDepartmentStepMobile1RegistrationNumbers/>
</div>
<!-- Buttons -->
<div class="is-flex is-justify-content-center mt-6">
<GenericButton class="white">Next</GenericButton>
</div>
<!-- Additional content or components can be added here -->
</template>
</template>
<style scoped>
@@ -0,0 +1,207 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import PosDepartmentStepMobile1RegistrationNumberInputFieldLabel
from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1RegistrationNumberInputFieldLabel.vue";
import VerifiedCustomer from "@/components/viewport/elements/icons/VerifiedCustomer.vue";
const props = defineProps({
modelValue: {
type: String,
default: ''
},
label: {
type: String,
default: ''
}
});
const emit = defineEmits(['update:modelValue']);
const modelValue = ref(props.modelValue);
watch(() => props.modelValue, (newValue) => {
modelValue.value = newValue;
});
watch(modelValue, (newValue) => {
emit('update:modelValue', newValue);
});
</script>
<template>
<div>
<PosDepartmentStepMobile1RegistrationNumberInputFieldLabel>{{ label }}</PosDepartmentStepMobile1RegistrationNumberInputFieldLabel>
<div class="columns is-mobile is-vcentered mb-0 is-multiline">
<div class="column m-0 pb-0">
<div class="field">
<div class="control">
<input
class="custom-input"
:class="{ 'is-searched': $slots.searchResults }"
type="text"
v-model="modelValue"
/>
</div>
</div>
</div>
<!-- If slot is provided, render it -->
<div class="column pl-0 is-narrow pb-0" v-if="$slots.default && !$slots.searchResults">
<slot />
</div>
<!-- If slot is provided, render it -->
<div class="column is-12 pt-0" v-if="$slots.searchResults">
<div class="search-results-container">
<!-- Demonstration of search results -->
<div v-for="i in 5" :key="i" style="border-bottom: 1px solid #EAEAEA">
<div class="columns is-mobile">
<div class="column is-align-content-center pl-4">
<p>Customer name</p>
</div>
<div class="column is-narrow pr-5 py-4">
<!-- Icon -->
<div class="custom-icon" style="width: 28px; height: 28px; border-bottom: none">
<VerifiedCustomer/>
</div>
</div>
</div>
</div>
<!-- Render search results slot -->
<slot name="searchResults" />
</div>
</div>
</div>
</div>
</template>
<style scoped>
.custom-input {
/* input-fields */
box-sizing: border-box;
/* Auto layout */
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 0px 8px;
gap: 10px;
width: 100%;
height: 42px;
background: #FFFFFF;
border: 1px solid #A6A5A5;
border-radius: 4px;
/* Inside auto layout */
flex: none;
order: 1;
align-self: stretch;
flex-grow: 0;
}
.custom-input.is-searched {
/* input-fields */
box-sizing: border-box;
/* Auto layout */
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 12px 8px;
gap: 10px;
width: 100%;
height: 42px;
background: #FFFFFF;
border: 1px solid #000000;
border-radius: 4px;
border-bottom: none;
/* Inside auto layout */
flex: none;
order: 1;
align-self: stretch;
flex-grow: 0;
}
.search-results-container {
/* Frame 59 */
box-sizing: border-box;
/* Auto layout */
display: flex;
flex-direction: column;
padding: 0px;
width: 100%;
height: max-content;
background: #FFFFFF;
border: 1px solid #A6A5A5;
}
.search-results-container > div .column div {
/* input-fields */
box-sizing: border-box;
/* Auto layout */
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
gap: 10px;
width: 100%;
height: 36px;
border-bottom: 1px solid #EAEAEA;
/* Inside auto layout */
flex: none;
order: 0;
align-self: stretch;
flex-grow: 0;
}
.search-results-container > div p {
/* */
margin: 0 auto;
height: 12px;
width: 100%;
/* sm-tx */
font-family: 'Arial';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 100%;
/* identical to box height, or 12px */
color: #808080;
/* Inside auto layout */
flex: none;
order: 0;
flex-grow: 0;
}
.custom-icon > svg {
/* icon */
width: 28px;
height: 28px;
/* Auto layout */
flex: none;
order: 0;
flex-grow: 0;
align-self: center;
margin: 0 auto;
}
</style>
@@ -0,0 +1,36 @@
<script setup lang="ts">
</script>
<template>
<label class="custom-text">
<slot></slot>
</label>
</template>
<style scoped>
.custom-text {
/* Category Tag */
width: 100%;
height: 12px;
/* sm-tx */
font-family: 'Arial';
font-style: normal;
font-weight: 400;
font-size: 12px;
line-height: 100%;
/* identical to box height, or 12px */
color: #000000;
/* Inside auto layout */
flex: none;
order: 0;
align-self: stretch;
flex-grow: 0;
}
</style>
@@ -0,0 +1,137 @@
<script setup lang="ts">
import { ref, defineEmits } from "vue";
import { reg_1, reg_2, reg_3 } from "@/components/shop/POSDepartmentProcess.vue";
import PosDepartmentStepMobile1RegistrationNumberInputField from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1RegistrationNumberInputField.vue";
import BarcodeScanner from "@/components/viewport/elements/icons/BarcodeScanner.vue";
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
// Define the close event to emit when the component is closed
const emit = defineEmits(["close"]);
// Define a reactive variable to control the expanded state of the component
const expanded = ref(false);
// Define reactive variables for registration numbers and labels
const registrationNumber1 = ref("");
const label1 = ref("Reg 1*");
const registrationNumber2 = ref("");
const label2 = ref("Reg 2*");
const registrationNumber3 = ref("");
const label3 = ref("Reg 3");
</script>
<template>
<div class="">
<!-- First input field with a required label -->
<PosDepartmentStepMobile1RegistrationNumberInputField
v-model:modelValue="reg_1"
v-model:label="label1"
/>
<!-- Second input field with a custom expander button -->
<PosDepartmentStepMobile1RegistrationNumberInputField
v-model:modelValue="reg_2"
v-model:label="label2"
>
<div class="custom-expander">
<span class="custom-text" @click="expanded = !expanded">+</span>
</div>
<template #searchResults></template>
</PosDepartmentStepMobile1RegistrationNumberInputField>
<!-- Third input field that is conditionally displayed based on the expanded state -->
<PosDepartmentStepMobile1RegistrationNumberInputField
v-show="expanded"
v-model:modelValue="registrationNumber3"
v-model:label="label3"
/>
<!-- Scan instead of manual input
<button class="button p-3 is-fullwidth mt-3 is-text" style="text-decoration: none" @click="$emit('close')">
<span class="icon is-small">
<i class="fas fa-camera"></i>
</span>
<span class="is-size-7 has-text-weight-semibold">Scan instead of manual input</span>
</button>
-->
<button class="button p-5 is-fullwidth mt-3 is-text" style="text-decoration: none" @click="$emit('close')">
<span class="m-2"><BarcodeScanner/></span>
<span class="custom-label-button">Or scan plates</span>
</button>
<!-- Add vehicle button -->
<GenericButton class="p-5">
Add vehicle
</GenericButton>
{{ reg_1 }}
{{ reg_2 }}
{{ reg_3 }}
</div>
</template>
<style scoped>
.custom-label-button {
/* Drop in and pay at location */
width: max-content;
height: 18px;
font-family: 'Arial';
font-style: normal;
font-weight: 400;
font-size: 16px;
line-height: 18px;
letter-spacing: -0.01em;
text-decoration-line: underline;
text-decoration-color: #000000;
text-decoration-thickness: 1px;
text-underline-offset: 3px;
color: #000000;
/* Inside auto layout */
flex: none;
order: 1;
flex-grow: 0;
}
.custom-expander {
/* Button */
/* Auto layout */
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 8px 12px;
gap: 10px;
width: 34px;
height: 34px;
background: #000000;
border-radius: 4px;
/* Inside auto layout */
flex: none;
order: 1;
flex-grow: 0;
}
.custom-text {
/* + */
height: 18px;
width: max-content;
font-family: 'Arial';
font-style: normal;
font-weight: 700;
font-size: 16px;
line-height: 18px;
color: #FFFFFF;
/* Inside auto layout */
flex: none;
order: 0;
flex-grow: 0;
}
</style>
+2 -1
View File
@@ -2,10 +2,11 @@
import ViewportHeader from "@/components/viewport/page/headers/ViewportHeader.vue";
import ViewportContent from "@/components/viewport/page/content/ViewportContent.vue";
import { backgroundColor } from "./page/headers/ViewportHeaderSettings.vue";
</script>
<template>
<div class="viewport">
<div class="viewport" :style="{ backgroundColor: backgroundColor }">
<ViewportHeader/>
<ViewportContent>
<slot/>
@@ -0,0 +1,41 @@
<script setup lang="ts">
</script>
<template>
<svg width="32" height="23" viewBox="0 0 32 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_481_2924)">
<path d="M5.58373 17.0005H3.40521C3.18736 17.0005 3.00391 16.8976 3.00391 16.7754V6.17627C3.00391 6.05408 3.18736 5.95117 3.40521 5.95117H5.58373C5.80158 5.95117 5.98504 6.05408 5.98504 6.17627V16.7754C5.98504 16.8976 5.80158 17.0005 5.58373 17.0005Z" fill="black"/>
<path d="M19.5037 17.0005H17.3251C17.1073 17.0005 16.9238 16.8976 16.9238 16.7754V6.17627C16.9238 6.05408 17.1073 5.95117 17.3251 5.95117H19.5037C19.7215 5.95117 19.905 6.05408 19.905 6.17627V16.7754C19.905 16.8976 19.7215 17.0005 19.5037 17.0005Z" fill="black"/>
<path d="M9.91816 17.0005H8.64545C8.42759 17.0005 8.24414 16.8976 8.24414 16.7754V6.17627C8.24414 6.05408 8.42759 5.95117 8.64545 5.95117H9.91816C10.136 5.95117 10.3195 6.05408 10.3195 6.17627V16.7754C10.3195 16.8976 10.136 17.0005 9.91816 17.0005Z" fill="black"/>
<path d="M14.2521 17.0005H12.9794C12.7616 17.0005 12.5781 16.8976 12.5781 16.7754V6.17627C12.5781 6.05408 12.7616 5.95117 12.9794 5.95117H14.2521C14.47 5.95117 14.6535 6.05408 14.6535 6.17627V16.7754C14.6535 16.8976 14.4815 17.0005 14.2521 17.0005Z" fill="black"/>
<path d="M23.8615 17.0005H22.5888C22.371 17.0005 22.1875 16.8976 22.1875 16.7754V6.17627C22.1875 6.05408 22.371 5.95117 22.5888 5.95117H23.8615C24.0794 5.95117 24.2628 6.05408 24.2628 6.17627V16.7754C24.2628 16.8976 24.0794 17.0005 23.8615 17.0005Z" fill="black"/>
<path d="M28.1936 17.0005H26.9208C26.703 17.0005 26.5195 16.8976 26.5195 16.7754V6.17627C26.5195 6.05408 26.703 5.95117 26.9208 5.95117H28.1936C28.4114 5.95117 28.5949 6.05408 28.5949 6.17627V16.7754C28.5949 16.8976 28.4114 17.0005 28.1936 17.0005Z" fill="black"/>
<path d="M3.70348 0H0.401306C0.183454 0 0 0.183454 0 0.401306V3.70348C0 3.92134 0.183454 4.10479 0.401306 4.10479H1.43324C1.65109 4.10479 1.83454 3.92134 1.83454 3.70348V2.23585C1.83454 2.018 2.018 1.83454 2.23585 1.83454H3.70348C3.92134 1.83454 4.10479 1.65109 4.10479 1.43324V0.401306C4.10479 0.171988 3.92134 0 3.70348 0Z" fill="black"/>
<path d="M27.4961 0.401306V1.43324C27.4961 1.65109 27.6795 1.83454 27.8974 1.83454H29.365C29.5829 1.83454 29.7663 2.018 29.7663 2.23585V3.70348C29.7663 3.92134 29.9498 4.10479 30.1676 4.10479H31.1996C31.4174 4.10479 31.6009 3.92134 31.6009 3.70348V0.401306C31.6009 0.183454 31.4174 0 31.1996 0H27.8974C27.6795 0 27.4961 0.171988 27.4961 0.401306Z" fill="black"/>
<path d="M1.43324 19H0.401306C0.183454 19 0 19.1835 0 19.4013V22.7035C0 22.9213 0.183454 23.1048 0.401306 23.1048H3.70348C3.92134 23.1048 4.10479 22.9213 4.10479 22.7035V21.6716C4.10479 21.4537 3.92134 21.2702 3.70348 21.2702H2.23585C2.018 21.2702 1.83454 21.0868 1.83454 20.8689V19.4013C1.83454 19.172 1.65109 19 1.43324 19Z" fill="black"/>
<path d="M29.7663 19.4931V20.9607C29.7663 21.1786 29.5829 21.362 29.365 21.362H27.8974C27.6795 21.362 27.4961 21.5455 27.4961 21.7634V22.7953C27.4961 23.0131 27.6795 23.1966 27.8974 23.1966H31.1996C31.4174 23.1966 31.6009 23.0131 31.6009 22.7953V19.4931C31.6009 19.2753 31.4174 19.0918 31.1996 19.0918H30.1676C29.9498 19.0918 29.7663 19.2753 29.7663 19.4931Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_481_2924">
<rect width="32" height="23" fill="white"/>
</clipPath>
</defs>
</svg>
</template>
<style scoped>
/* Setting the center of the SVG */
svg {
display: block;
margin: auto;
width: 32px;
height: 23px;
}
/* Ensuring the SVG scales properly */
svg {
width: 100%;
height: auto;
}
</style>
@@ -0,0 +1,20 @@
<script setup lang="ts">
</script>
<template>
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="28" height="28" rx="3.29412" fill="#80FF93"/>
<g clip-path="url(#clip0_481_2994)">
<path d="M18.548 15.2676C17.873 13.8965 16.6284 12.7715 15.2152 12.2266C16.4667 11.4497 17.307 10.068 17.307 8.48952C17.0679 2.66066 8.7535 2.65362 8.51092 8.48952C8.51092 10.068 9.35115 11.4532 10.6062 12.2301C8.2578 13.1512 6.58789 15.4363 6.58789 18.1117C6.58789 19.4723 7.6953 20.5832 9.05936 20.5832H13.2289C13.9531 20.5691 13.9531 19.4969 13.2289 19.4863H9.06287C8.3035 19.4863 7.68827 18.8676 7.68827 18.1082C7.68827 15.2289 10.0297 12.8875 12.9125 12.8875C14.3011 12.8875 15.6124 13.4325 16.6003 14.4203C16.9835 14.8035 17.2999 15.236 17.5425 15.707C16.2523 16.4734 15.3839 17.8797 15.3839 19.4863C15.3839 21.9086 17.3562 23.8808 19.7819 23.8808C22.2077 23.8808 24.18 21.9086 24.18 19.4863C24.2257 16.6035 21.2901 14.4133 18.548 15.2676ZM12.9125 11.7872C11.0949 11.7872 9.61834 10.3141 9.61482 8.49304C9.61482 6.67196 11.0914 5.19189 12.9125 5.19189C17.2788 5.34657 17.2753 11.636 12.9125 11.7872ZM16.4843 19.4863C16.4843 17.6688 17.9644 16.1887 19.7855 16.1887C21.603 16.1887 23.0831 17.6652 23.0831 19.4863C22.9249 23.8527 16.639 23.8492 16.4843 19.4863ZM20.6292 18.2734L19.3706 19.532L18.9347 19.0961C18.4249 18.5898 17.6515 19.3633 18.1577 19.873L18.9804 20.6992C19.1948 20.9137 19.5429 20.9137 19.7573 20.6992L21.4061 19.0504C21.9124 18.5406 21.1425 17.7637 20.6292 18.2734Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_481_2994">
<rect width="17.5921" height="19.7647" fill="white" transform="translate(6.58789 4.11719)"/>
</clipPath>
</defs>
</svg>
</template>
<style scoped>
</style>
@@ -4,6 +4,10 @@ const isOpen = ref(false); // State of the navigation drawer (Expanded or co
const backgroundColor = ref('#ffffff'); // Background color of header
const defaultImagePath = '@/assets/branding/truckwash-banner-white-compressed.png'
const isTransparent = ref(false); // State for transparency of the header
const backgroundColors = {
default: '#ffffff',
blue: '#e6efff',
}
// Function to toggle the navigation drawer
const toggleExpanded = () => {
@@ -31,9 +35,10 @@ export default defineComponent({
defaultImagePath,
isTransparent,
setTransparency,
backgroundColors,
};
},
});
export { toggleExpanded, setBackgroundColor, isOpen, backgroundColor, defaultImagePath, isTransparent, setTransparency };
export { toggleExpanded, setBackgroundColor, isOpen, backgroundColor, defaultImagePath, isTransparent, setTransparency, backgroundColors };
</script>
@@ -23,7 +23,7 @@
align-items: center;
padding: 8px 12px;
gap: 10px;
width: 304px;
width: 100%;
height: 34px;
background: #000000;
border-radius: 4px;
@@ -61,7 +61,7 @@
align-items: center;
padding: 8px 12px;
gap: 10px;
width: 304px;
width: 100%;
height: 34px;
background: #A6A5A5;
border-radius: 4px;
@@ -99,7 +99,7 @@
align-items: center;
padding: 8px 12px;
gap: 10px;
width: 304px;
width: 100%;
height: 34px;
background: #FFFFFF;
border-radius: 4px;
@@ -113,7 +113,6 @@ defineExpose({
isCameraActive,
startCamera,
stopCamera,
getFrame
});
watch(() => isCameraActive.value, (newVal) => {
@@ -123,10 +122,7 @@ watch(() => isCameraActive.value, (newVal) => {
// Emit a picture every 10 seconds
setInterval(() => {
if (isCameraActive.value) {
const frame = getFrame();
if (frame) {
emits('scanner-toggled', true);
}
getFrame();
}
}, 10000);
</script>