Files
pleno-vue/src/components/displays/department/pos/steps/mobile/views/PosDepartmentStep1MobileManualInput.vue
T
Jeppe B 1729443cc3 Resolve frontend Qodana critical and high findings (#176)
Resolve recommended-profile Critical and High findings, update vulnerable dependencies, restore invoice queue E2E authentication setup, and clear the remaining frontend Qodana findings.
2026-07-17 06:22:51 +02:00

449 lines
13 KiB
Vue

<script setup lang="ts">
import { ref, onBeforeUnmount, onMounted, watch } from "vue";
import {
order_id,
reg_1,
reg_2,
reg_3,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} 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 "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
import RegistrationNumberSearchResult from "@/components/models/pos/step1/RegistrationNumberSearchResult.vue";
import "@/components/viewport/elements/icons/VerifiedCustomer.vue";
import { pos } from "../objects/PosDepartmentStepMobileFlow.vue";
import type { PosSearchResult } from "../objects/PosSearchResult.vue";
import SessionUser from "@/components/session/token/SessionUser.vue";
import PosDepartmentStepMobileFixedBottomControl from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileFixedBottomControl.vue";
import PosDepartmentStepMobileButtonNextStep from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobileButtonNextStep.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
// Define the close event to emit when the component is closed
const emit = defineEmits(["close"]);
// Define the props
const props = defineProps({
// Show Reg 1.
showReg1: {
type: Boolean,
default: true,
},
showReg2: {
type: Boolean,
default: true,
},
showReg3: {
type: Boolean,
default: true,
},
showButtons: {
type: Boolean,
default: true,
},
forceShowApplicable: {
// Used to show reg 3, when reg 2 is hidden.
type: Boolean,
default: false,
},
});
const inputTestIds = {
reg1: "pos-mobile-reg-input-1",
reg2: "pos-mobile-reg-input-2",
reg3: "pos-mobile-reg-input-3",
};
const normalizeRegistrationValue = (value: string | null | undefined) => String(value ?? "").toUpperCase();
const getNormalizedOrderId = () => {
const parsedOrderId = Number.parseInt(String(order_id.value), 10);
return Number.isInteger(parsedOrderId) && parsedOrderId > 0 ? parsedOrderId : null;
};
const getRegistrationSourceRef = (vehicleIndex: number) => {
switch (vehicleIndex) {
case 1:
return reg_1;
case 2:
return reg_2;
case 3:
return reg_3;
default:
return reg_1;
}
};
const setPersistedRegistrationValue = (vehicleIndex: number, value: string) => {
getRegistrationSourceRef(vehicleIndex).value = normalizeRegistrationValue(value);
};
const syncVehicleRegistration = (vehicleIndex: number, value: string, overrides: Record<string, any> = {}) => {
const normalizedValue = normalizeRegistrationValue(value);
if (normalizedValue === "") {
pos.vehicles.select(vehicleIndex, null);
return;
}
const currentVehicle = pos.vehicles.get(vehicleIndex);
pos.vehicles.select(vehicleIndex, {
...(currentVehicle ?? {}),
...overrides,
reg: normalizedValue,
});
};
// Define a reactive variable to control the expanded state of the component
const expanded = ref(props.forceShowApplicable);
const label1 = ref("Reg 1*");
const label2 = ref("Reg 2*");
const label3 = ref("Reg 3");
const createRegistrationAutosave = (vehicleIndex: number) =>
useOrderMetadataAutosave({
source: getRegistrationSourceRef(vehicleIndex),
normalizeValue: normalizeRegistrationValue,
saveValue: async (value) => {
const normalizedOrderId = getNormalizedOrderId();
if (!normalizedOrderId) {
return value;
}
return saveOrderMetadataField(`reg_${vehicleIndex}`, value, normalizedOrderId);
},
onSaved: async (value) => {
setPersistedRegistrationValue(vehicleIndex, value);
syncVehicleRegistration(vehicleIndex, value);
},
});
const registrationAutosaves = {
1: createRegistrationAutosave(1),
2: createRegistrationAutosave(2),
3: createRegistrationAutosave(3),
};
const reg1Draft = registrationAutosaves[1].draft;
const reg2Draft = registrationAutosaves[2].draft;
const reg3Draft = registrationAutosaves[3].draft;
const setRegistrationDraft = (vehicleIndex: number, value: string) => {
const normalizedValue = normalizeRegistrationValue(value);
switch (vehicleIndex) {
case 1:
reg1Draft.value = normalizedValue;
break;
case 2:
reg2Draft.value = normalizedValue;
break;
case 3:
reg3Draft.value = normalizedValue;
break;
default:
break;
}
};
watch(reg1Draft, (value) => {
syncVehicleRegistration(1, value);
if (getNormalizedOrderId()) {
registrationAutosaves[1].scheduleSave();
return;
}
setPersistedRegistrationValue(1, value);
});
watch(reg2Draft, (value) => {
syncVehicleRegistration(2, value);
if (getNormalizedOrderId()) {
registrationAutosaves[2].scheduleSave();
return;
}
setPersistedRegistrationValue(2, value);
});
watch(reg3Draft, (value) => {
syncVehicleRegistration(3, value);
if (getNormalizedOrderId()) {
registrationAutosaves[3].scheduleSave();
return;
}
setPersistedRegistrationValue(3, value);
});
watch(
() => getNormalizedOrderId(),
(normalizedOrderId) => {
if (!normalizedOrderId) {
return;
}
[1, 2, 3].forEach((vehicleIndex) => {
if (registrationAutosaves[vehicleIndex].isDirty.value) {
registrationAutosaves[vehicleIndex].scheduleSave();
}
});
}
);
const flushRegistration = async (vehicleIndex: number) => {
await registrationAutosaves[vehicleIndex].flush();
};
const flushAllRegistrations = async () => {
await Promise.all([1, 2, 3].map((vehicleIndex) => registrationAutosaves[vehicleIndex].flush()));
};
const waitForAllRegistrationSaves = async () => {
await Promise.all([1, 2, 3].map((vehicleIndex) => registrationAutosaves[vehicleIndex].flushAndWait()));
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(waitForAllRegistrationSaves);
// Function to set a value if it is not null
const createVehicleSelectionFromSearchResult = (value: PosSearchResult) => ({
...value,
reg: value.registrationNumber,
status: value.customerStatus,
customer_id: value.customerId,
last_order_id: value.lastOrderId,
booking_id: value.bookingId,
booking_matches: value.bookingMatches || [],
wash_subscription: value.washSubscription,
});
function setIfNotNull(variable: string, value: PosSearchResult | null) {
//console.warn("setIfNotNull", variable, value);
if (value !== null) {
const nextVehicleSelection = createVehicleSelectionFromSearchResult(value);
switch (variable) {
case "reg_1":
//console.log("Setting reg_1 with value:", value);
pos.vehicles.select(1, nextVehicleSelection);
setRegistrationDraft(1, value.registrationNumber);
//reg_1_status.value = value.customerStatus;
break;
case "reg_2":
pos.vehicles.select(2, nextVehicleSelection);
setRegistrationDraft(2, value.registrationNumber);
//reg_2_status.value = value.customerStatus;
break;
case "reg_3":
pos.vehicles.select(3, nextVehicleSelection);
setRegistrationDraft(3, value.registrationNumber);
//reg_3_status.value = value.customerStatus;
break;
}
} else {
console.warn("setIfNotNull called with null value for", variable);
}
}
// Function to set the value of a user input
// This is used to:
// - Update the registration number to one not in the list
// - Clear the registration number, if the user input is empty
// - Automatically select the first vehicle in the list, if the user input matches one of the vehicles exactly
function setValueUserInput(vehicleIndex: number, value: string) {
//console.log("setValueUserInput", vehicleIndex, value);
const normalizedValue = normalizeRegistrationValue(value);
/** Case: Empty input */
// If the value is empty, clear the registration number
if (normalizedValue === "") {
pos.vehicles.select(vehicleIndex, null);
setRegistrationDraft(vehicleIndex, "");
void flushRegistration(vehicleIndex);
return;
}
setRegistrationDraft(vehicleIndex, normalizedValue);
void flushRegistration(vehicleIndex);
}
onMounted(() => {
// Initialize the registration numbers from the pos.vehicles object
setRegistrationDraft(1, pos.vehicles.get(1)?.reg ?? reg_1.value ?? "");
setRegistrationDraft(2, pos.vehicles.get(2)?.reg ?? reg_2.value ?? "");
setRegistrationDraft(3, pos.vehicles.get(3)?.reg ?? reg_3.value ?? "");
// If the reg_3 is not empty, set the expanded state to true
expanded.value = reg_3.value !== "" || props.forceShowApplicable;
});
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
void flushAllRegistrations();
});
const closeManualInput = async () => {
await waitForAllRegistrationSaves();
emit("close");
};
</script>
<template>
<div data-testid="pos-mobile-manual-input">
<template v-if="props.showReg1">
<!-- First input field with a required label -->
<PosDepartmentStepMobile1RegistrationNumberInputField
v-model:modelValue="reg1Draft"
v-model:label="label1"
:inputTestId="inputTestIds.reg1"
@focusout="setValueUserInput(1, reg1Draft)"
>
<template #searchResults>
<RegistrationNumberSearchResult
@select="setIfNotNull('reg_1', $event)"
:searchQuery="reg1Draft"
:automaticallySelect="true"
/>
</template>
</PosDepartmentStepMobile1RegistrationNumberInputField>
</template>
<template v-if="props.showReg2">
<!-- Second input field with a custom expander button -->
<PosDepartmentStepMobile1RegistrationNumberInputField
v-model:modelValue="reg2Draft"
v-model:label="label2"
:inputTestId="inputTestIds.reg2"
@focusout="setValueUserInput(2, reg2Draft)"
>
<template #default>
<div class="custom-expander" @click="expanded = !expanded" v-if="!reg3Draft">
<span class="custom-text">+</span>
</div>
</template>
<template #searchResults>
<RegistrationNumberSearchResult
@select="setIfNotNull('reg_2', $event)"
:searchQuery="reg2Draft"
:modifyCustomerOnChange="false"
:automaticallySelect="true"
/>
</template>
</PosDepartmentStepMobile1RegistrationNumberInputField>
</template>
<template v-if="props.showReg3">
<!-- Third input field that is conditionally displayed based on the expanded state -->
<PosDepartmentStepMobile1RegistrationNumberInputField
v-show="expanded || reg3Draft !== ''"
v-model:modelValue="reg3Draft"
v-model:label="label3"
:inputTestId="inputTestIds.reg3"
@focusout="setValueUserInput(3, reg3Draft)"
>
<template #searchResults>
<RegistrationNumberSearchResult
@select="setIfNotNull('reg_3', $event)"
:searchQuery="reg3Draft"
:modifyCustomerOnChange="false"
:automaticallySelect="true"
/>
</template>
</PosDepartmentStepMobile1RegistrationNumberInputField>
</template>
<template v-if="props.showButtons">
<!-- Back to scan button -->
<button
class="button p-5 is-fullwidth mt-3 is-text"
data-testid="pos-mobile-manual-input-close"
style="text-decoration: none"
@click="closeManualInput()"
>
<span class="m-2"><BarcodeScanner /></span>
<span class="custom-label-button">{{ SessionUser.objects.global.language.or_scan_plates }}</span>
</button>
</template>
<!-- Buttons -->
<PosDepartmentStepMobileFixedBottomControl v-if="props.showButtons">
<!-- Close button -->
<PosDepartmentStepMobileButtonNextStep
:isWhite="false"
:customAction="() => closeManualInput()"
:customDisabled="false"
:buttonClasses="['has-background-primary', 'has-text-black']"
action-key="pos-mobile-manual-input-close-action"
copy-key="close"
>
<span class="pos-mobile-action-content">
<span class="pos-mobile-action-label">{{ SessionUser.objects.global.language.close }}</span>
<span class="pos-mobile-action-icon">
<i class="fa-solid fa-xmark"></i>
</span>
</span>
</PosDepartmentStepMobileButtonNextStep>
</PosDepartmentStepMobileFixedBottomControl>
</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>