Resolve recommended-profile Critical and High findings, update vulnerable dependencies, restore invoice queue E2E authentication setup, and clear the remaining frontend Qodana findings.
449 lines
13 KiB
Vue
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>
|