Compare commits

...
Author SHA1 Message Date
Jeppe Bundgaard 6463a3d211 Preserve registration save payload formatting 2026-07-06 10:03:19 +02:00
Jeppe Bundgaard e792f5820d Fix POS step metadata persistence 2026-07-06 09:34:01 +02:00
13 changed files with 640 additions and 155 deletions
@@ -1,9 +1,12 @@
<script setup> <script setup>
import { computed, nextTick, ref, watch } from "vue"; import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js"; import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import { isBlankPosMetadataValue } from "@/components/shop/POSDepartmentProcess.vue"; import {
isBlankPosMetadataValue,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({ const props = defineProps({
order_id: { order_id: {
@@ -123,7 +126,7 @@ const referenceField = createCustomerWishField({
key: "reference", key: "reference",
label: computed(() => t("common.reference")), label: computed(() => t("common.reference")),
source: () => props.reference, source: () => props.reference,
saveValue: (value) => SessionUser.objects.orders.set.reference(props.order_id, value), saveValue: (value) => saveOrderMetadataField("reference", value, props.order_id),
testIdBase: "pos-order-customer-wishes-reference", testIdBase: "pos-order-customer-wishes-reference",
isRequired: () => props.referenceRequired, isRequired: () => props.referenceRequired,
warningStateWhenEmpty: "danger", warningStateWhenEmpty: "danger",
@@ -134,7 +137,7 @@ const poField = createCustomerWishField({
key: "po", key: "po",
label: computed(() => t("admin.pos.po_number")), label: computed(() => t("admin.pos.po_number")),
source: () => props.po, source: () => props.po,
saveValue: (value) => SessionUser.objects.orders.set.po(props.order_id, value), saveValue: (value) => saveOrderMetadataField("po", value, props.order_id),
testIdBase: "pos-order-customer-wishes-po", testIdBase: "pos-order-customer-wishes-po",
isRequired: () => props.poRequired, isRequired: () => props.poRequired,
warningStateWhenEmpty: "warning", warningStateWhenEmpty: "warning",
@@ -145,7 +148,7 @@ const safetySealField = createCustomerWishField({
key: "safety_seal", key: "safety_seal",
label: computed(() => t("admin.pos.safety_seal")), label: computed(() => t("admin.pos.safety_seal")),
source: () => props.safetySeal, source: () => props.safetySeal,
saveValue: (value) => SessionUser.objects.orders.set.safety_seal(props.order_id, value), saveValue: (value) => saveOrderMetadataField("safety_seal", value, props.order_id),
testIdBase: "pos-order-customer-wishes-safety-seal", testIdBase: "pos-order-customer-wishes-safety-seal",
isRequired: () => false, isRequired: () => false,
warningStateWhenEmpty: null, warningStateWhenEmpty: null,
@@ -161,6 +164,15 @@ const safetySealField = createCustomerWishField({
const fields = computed(() => { const fields = computed(() => {
return props.showSafetySeal ? [referenceField, poField, safetySealField] : [referenceField, poField]; return props.showSafetySeal ? [referenceField, poField, safetySealField] : [referenceField, poField];
}); });
const flushCustomerWishFields = async () => {
await Promise.all(fields.value.map((field) => field.autosave.flushAndWait()));
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(flushCustomerWishFields);
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script> </script>
<template> <template>
@@ -1,8 +1,11 @@
<script setup> <script setup>
import { computed, nextTick, ref, watch } from 'vue'; import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js"; import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import {
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({ const props = defineProps({
order_id: { order_id: {
@@ -30,8 +33,7 @@ const isEditing = ref(false);
const autosave = useOrderMetadataAutosave({ const autosave = useOrderMetadataAutosave({
source: () => props.note, source: () => props.note,
saveValue: async (value) => { saveValue: async (value) => {
await SessionUser.objects.orders.set.notes(props.order_id, value); return saveOrderMetadataField("notes", value, props.order_id);
return value;
}, },
onSaved: async (savedValue, requestedValue) => { onSaved: async (savedValue, requestedValue) => {
if (typeof props.onSaved === "function") { if (typeof props.onSaved === "function") {
@@ -68,6 +70,12 @@ const lostFocus = async () => {
isEditing.value = false; isEditing.value = false;
await autosave.flush(); await autosave.flush();
}; };
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(() => autosave.flushAndWait());
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script> </script>
<template> <template>
@@ -1,8 +1,11 @@
<script setup> <script setup>
import { computed, nextTick, ref, watch } from "vue"; import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js"; import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
import {
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue";
const props = defineProps({ const props = defineProps({
index: { index: {
@@ -36,8 +39,7 @@ autosave = useOrderMetadataAutosave({
source: () => props.modelValue, source: () => props.modelValue,
normalizeValue: normalizeRegistrationDraft, normalizeValue: normalizeRegistrationDraft,
saveValue: async (value) => { saveValue: async (value) => {
await SessionUser.objects.orders.set[fieldKey.value](props.order_id, value); return saveOrderMetadataField(fieldKey.value, value, props.order_id);
return value;
}, },
onSaved: async () => { onSaved: async () => {
await props.loadOrder(); await props.loadOrder();
@@ -74,6 +76,12 @@ const closeEditor = async () => {
isEditing.value = false; isEditing.value = false;
await autosave.flush(); await autosave.flush();
}; };
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(() => autosave.flushAndWait());
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
</script> </script>
<template> <template>
@@ -4,7 +4,7 @@ import {
setBackgroundColor, setBackgroundColor,
backgroundColors, backgroundColors,
} from "@/components/viewport/page/headers/ViewportHeaderSettings.vue"; } from "@/components/viewport/page/headers/ViewportHeaderSettings.vue";
import { onMounted, ref, watch, computed } from "vue"; import { onBeforeUnmount, onMounted, ref, watch, computed } from "vue";
import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue";
import PosDepartmentStepMobile2Product from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue"; import PosDepartmentStepMobile2Product from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue";
import PosDepartmentStepMobile2LastOrder from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue"; import PosDepartmentStepMobile2LastOrder from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue";
@@ -43,6 +43,8 @@ import {
customer_name, customer_name,
isAddonRestricted, isAddonRestricted,
canBuyAdditionalServices, canBuyAdditionalServices,
registerPosStepSaveBarrier,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue"; } from "@/components/shop/POSDepartmentProcess.vue";
import { createOrderItem, getOrderItems, removeOrderItem } from "@/components/shop/OrdersItems.vue"; import { createOrderItem, getOrderItems, removeOrderItem } from "@/components/shop/OrdersItems.vue";
import { PosProduct } from "@/components/displays/department/pos/steps/mobile/objects/PosProduct.vue"; import { PosProduct } from "@/components/displays/department/pos/steps/mobile/objects/PosProduct.vue";
@@ -196,8 +198,7 @@ const applyPendingBookingFromSelection = async () => {
// Set PO number on the order if provided by the booking // Set PO number on the order if provided by the booking
if (booking.po && typeof booking.po === "string" && booking.po.trim() !== "") { if (booking.po && typeof booking.po === "string" && booking.po.trim() !== "") {
try { try {
await SessionUser.objects.orders.set.po(order_id.value, booking.po); order_po.value = await saveOrderMetadataField("po", booking.po, order_id.value);
order_po.value = booking.po.trim();
} catch (e) { } catch (e) {
console.error("Failed to set PO on order from booking", e); console.error("Failed to set PO on order from booking", e);
} }
@@ -217,9 +218,9 @@ const applyPendingBookingFromSelection = async () => {
const bookingReference = String(fullBookingData.reference ?? fullBookingData.reference_number ?? "").trim(); const bookingReference = String(fullBookingData.reference ?? fullBookingData.reference_number ?? "").trim();
if (bookingReference !== "") { if (bookingReference !== "") {
try { try {
await SessionUser.objects.orders.set.reference(order_id.value, bookingReference); const savedReference = await saveOrderMetadataField("reference", bookingReference, order_id.value);
persistedReference.value = bookingReference; persistedReference.value = savedReference;
metadata.setReference(bookingReference); metadata.setReference(savedReference);
hydrateReferenceFromSources(); hydrateReferenceFromSources();
} catch (setRefError) { } catch (setRefError) {
console.error("Failed to set reference on order from booking:", setRefError); console.error("Failed to set reference on order from booking:", setRefError);
@@ -228,10 +229,10 @@ const applyPendingBookingFromSelection = async () => {
const bookingNotes = String(fullBookingData.notes ?? fullBookingData.note ?? "").trim(); const bookingNotes = String(fullBookingData.notes ?? fullBookingData.note ?? "").trim();
if (bookingNotes !== "") { if (bookingNotes !== "") {
try { try {
await SessionUser.objects.orders.set.notes(order_id.value, bookingNotes); const savedNotes = await saveOrderMetadataField("notes", bookingNotes, order_id.value);
order_notes.value = bookingNotes; order_notes.value = savedNotes;
notes.value = bookingNotes; notes.value = savedNotes;
metadata.setNotes(bookingNotes); metadata.setNotes(savedNotes);
} catch (setNotesError) { } catch (setNotesError) {
console.error("Failed to set notes on order from booking:", setNotesError); console.error("Failed to set notes on order from booking:", setNotesError);
} }
@@ -243,13 +244,11 @@ const applyPendingBookingFromSelection = async () => {
const currentReg2 = normalizeRegistrationValue(reg_2.value); const currentReg2 = normalizeRegistrationValue(reg_2.value);
if (bookingReg1 && bookingReg1 !== currentReg1) { if (bookingReg1 && bookingReg1 !== currentReg1) {
reg_1.value = bookingReg1; reg_1.value = await saveOrderMetadataField("reg_1", bookingReg1, order_id.value);
await SessionUser.objects.orders.set.reg_1(order_id.value, bookingReg1);
} }
if (bookingReg2 !== currentReg2) { if (bookingReg2 !== currentReg2) {
reg_2.value = bookingReg2; reg_2.value = await saveOrderMetadataField("reg_2", bookingReg2, order_id.value);
await SessionUser.objects.orders.set.reg_2(order_id.value, bookingReg2);
} }
const bookingMatches = Array.isArray(vehicles.vehicle_1.value?.booking_matches) const bookingMatches = Array.isArray(vehicles.vehicle_1.value?.booking_matches)
@@ -538,8 +537,7 @@ const notesAutosave = useOrderMetadataAutosave({
return value; return value;
} }
await SessionUser.objects.orders.set.notes(normalizedOrderId, value); return saveOrderMetadataField("notes", value, normalizedOrderId);
return value;
}, },
onSaved: async (value) => { onSaved: async (value) => {
order_notes.value = value; order_notes.value = value;
@@ -555,8 +553,7 @@ const referenceAutosave = useOrderMetadataAutosave({
return value; return value;
} }
await SessionUser.objects.orders.set.reference(normalizedOrderId, value); return saveOrderMetadataField("reference", value, normalizedOrderId);
return value;
}, },
onSaved: async (value) => { onSaved: async (value) => {
persistedReference.value = value; persistedReference.value = value;
@@ -572,8 +569,7 @@ const safetySealAutosave = useOrderMetadataAutosave({
return value; return value;
} }
await SessionUser.objects.orders.set.safety_seal(normalizedOrderId, value); return saveOrderMetadataField("safety_seal", value, normalizedOrderId);
return value;
}, },
onSaved: async (value) => { onSaved: async (value) => {
order_safety_seal.value = value; order_safety_seal.value = value;
@@ -585,6 +581,18 @@ const stepTwoNotesInput = notesAutosave.draft;
const stepTwoReferenceInput = referenceAutosave.draft; const stepTwoReferenceInput = referenceAutosave.draft;
const stepTwoSafetySealInput = safetySealAutosave.draft; const stepTwoSafetySealInput = safetySealAutosave.draft;
const showSafetySealField = computed(() => transactionItems.containsWashCertificate()); const showSafetySealField = computed(() => transactionItems.containsWashCertificate());
const flushStepTwoMetadataFields = async () => {
await Promise.all([
notesAutosave.flushAndWait(),
referenceAutosave.flushAndWait(),
safetySealAutosave.flushAndWait(),
]);
};
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(flushStepTwoMetadataFields);
onBeforeUnmount(() => {
unregisterStepSaveBarrier();
});
const resolveReferenceFromSources = () => { const resolveReferenceFromSources = () => {
const orderReference = normalizeReferenceValue(persistedReference.value); const orderReference = normalizeReferenceValue(persistedReference.value);
@@ -20,6 +20,7 @@ import {
hasAttribute, hasAttribute,
uploadAttachment, uploadAttachment,
restoreStoredPosOrderId, restoreStoredPosOrderId,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue"; } from "@/components/shop/POSDepartmentProcess.vue";
import * as POSDepartmentProcess from "@/components/shop/POSDepartmentProcess.vue"; import * as POSDepartmentProcess from "@/components/shop/POSDepartmentProcess.vue";
import { errors } from "@/components/request/HandleGlobalError.vue"; import { errors } from "@/components/request/HandleGlobalError.vue";
@@ -96,17 +97,30 @@ const props = defineProps({
}, },
}); });
const updateReference = () => { const getActiveOrderId = () => {
if (!!order_id.value && order_id.value > 0) SessionUser.objects.orders.set.reference(order_id.value, reference.value); const parsedOrderId = Number.parseInt(String(order_id.value ?? ""), 10);
return Number.isInteger(parsedOrderId) && parsedOrderId > 0 ? parsedOrderId : null;
}; };
const updateNotes = () => { const updateReference = async () => {
if (!!order_id.value && order_id.value > 0) SessionUser.objects.orders.set.notes(order_id.value, order_notes.value); const activeOrderId = getActiveOrderId();
if (activeOrderId) {
await saveOrderMetadataField("reference", reference.value, activeOrderId);
}
}; };
const updateSafetySeal = () => { const updateNotes = async () => {
if (!!order_id.value && order_id.value > 0) const activeOrderId = getActiveOrderId();
SessionUser.objects.orders.set.safety_seal(order_id.value, order_safety_seal.value); if (activeOrderId) {
await saveOrderMetadataField("notes", order_notes.value, activeOrderId);
}
};
const updateSafetySeal = async () => {
const activeOrderId = getActiveOrderId();
if (activeOrderId) {
await saveOrderMetadataField("safety_seal", order_safety_seal.value, activeOrderId);
}
}; };
// Helpers: extracted for clarity and reuse // Helpers: extracted for clarity and reuse
@@ -196,9 +210,9 @@ const step1 = async () => {
// Reset the vehicle selection // Reset the vehicle selection
views.vehicleSelection.value = false; views.vehicleSelection.value = false;
// Merge references and notes // Merge references and notes
mergeReferences(); await mergeReferences();
mergeNotes(); await mergeNotes();
mergeSafetySeal(); await mergeSafetySeal();
// We only need to check if the customer requires reference, if the field is empty. // We only need to check if the customer requires reference, if the field is empty.
if (!isNonEmptyString(reference.value)) { if (!isNonEmptyString(reference.value)) {
const isValid = await validateReferenceRequirements(); const isValid = await validateReferenceRequirements();
@@ -214,37 +228,39 @@ const mergeReferences = () => {
if (metadata.getReference() && metadata.getReference().length >= 1) { if (metadata.getReference() && metadata.getReference().length >= 1) {
// If there is no reference, set it to the registration number of vehicle 1 // If there is no reference, set it to the registration number of vehicle 1
reference.value = metadata.getReference(); reference.value = metadata.getReference();
updateReference(); return updateReference();
} else if (reference.value && reference.value.length >= 1) { } else if (reference.value && reference.value.length >= 1) {
// If there is a reference in the field, set it to the metadata // If there is a reference in the field, set it to the metadata
metadata.setReference(reference.value); metadata.setReference(reference.value);
updateReference(); return updateReference();
} else if (vehicles.vehicle_1.value?.reference && vehicles.vehicle_1.value?.reference.length >= 1) { } else if (vehicles.vehicle_1.value?.reference && vehicles.vehicle_1.value?.reference.length >= 1) {
// If there is no reference, set it to the registration number of vehicle 1 // If there is no reference, set it to the registration number of vehicle 1
reference.value = vehicles.vehicle_1.value?.reference; reference.value = vehicles.vehicle_1.value?.reference;
metadata.setReference(reference.value); metadata.setReference(reference.value);
updateReference(); return updateReference();
} }
return Promise.resolve();
}; };
const mergeNotes = () => { const mergeNotes = () => {
if (metadata.getNotes() && metadata.getNotes().length >= 1) { if (metadata.getNotes() && metadata.getNotes().length >= 1) {
order_notes.value = metadata.getNotes(); order_notes.value = metadata.getNotes();
updateNotes(); return updateNotes();
} else if (order_notes.value && order_notes.value.length >= 1) { } else if (order_notes.value && order_notes.value.length >= 1) {
metadata.setNotes(order_notes.value); metadata.setNotes(order_notes.value);
updateNotes(); return updateNotes();
} }
return Promise.resolve();
}; };
const mergeSafetySeal = () => { const mergeSafetySeal = () => {
const resolvedSafetySeal = getResolvedMobileSafetySeal(); const resolvedSafetySeal = getResolvedMobileSafetySeal();
order_safety_seal.value = resolvedSafetySeal; order_safety_seal.value = resolvedSafetySeal;
syncMobileSafetySealState(resolvedSafetySeal); syncMobileSafetySealState(resolvedSafetySeal);
updateSafetySeal(); return updateSafetySeal();
}; };
const finalizeStep1 = async () => { const finalizeStep1 = async () => {
mergeReferences(); await mergeReferences();
mergeNotes(); await mergeNotes();
mergeSafetySeal(); await mergeSafetySeal();
// Upload the attachments if there are any (And the order id has been created) // Upload the attachments if there are any (And the order id has been created)
if (attachments.getBase64().length > 0 && order_id.value > 0) { if (attachments.getBase64().length > 0 && order_id.value > 0) {
const uploadResults = await Promise.all( const uploadResults = await Promise.all(
@@ -5,6 +5,7 @@ import {
order_safety_seal, order_safety_seal,
reference, reference,
clearStoredPosOrderId, clearStoredPosOrderId,
saveOrderMetadataField,
} from "@/components/shop/POSDepartmentProcess.vue"; } from "@/components/shop/POSDepartmentProcess.vue";
import { metadata } from "./PosDepartmentStepMobileFlow.vue"; import { metadata } from "./PosDepartmentStepMobileFlow.vue";
@@ -52,9 +53,9 @@ export const persistCurrentMobileOrderMetadata = async () => {
order_notes.value = normalizedNotes; order_notes.value = normalizedNotes;
await Promise.all([ await Promise.all([
SessionUser.objects.orders.set.reference(normalizedOrderId, normalizedReference), saveOrderMetadataField("reference", normalizedReference, normalizedOrderId),
SessionUser.objects.orders.set.notes(normalizedOrderId, normalizedNotes), saveOrderMetadataField("notes", normalizedNotes, normalizedOrderId),
SessionUser.objects.orders.set.safety_seal(normalizedOrderId, normalizedSafetySeal), saveOrderMetadataField("safety_seal", normalizedSafetySeal, normalizedOrderId),
]); ]);
return normalizedOrderId; return normalizedOrderId;
@@ -1,6 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onBeforeUnmount, onMounted, watch } from "vue"; import { ref, onBeforeUnmount, onMounted, watch } from "vue";
import { order_id, reg_1, reg_2, reg_3, getOrderDetails } from "@/components/shop/POSDepartmentProcess.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 PosDepartmentStepMobile1RegistrationNumberInputField from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1RegistrationNumberInputField.vue";
import BarcodeScanner from "@/components/viewport/elements/icons/BarcodeScanner.vue"; import BarcodeScanner from "@/components/viewport/elements/icons/BarcodeScanner.vue";
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue"; import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
@@ -103,9 +110,7 @@ const createRegistrationAutosave = (vehicleIndex: number) =>
return value; return value;
} }
await SessionUser.objects.orders.set[`reg_${vehicleIndex}`](normalizedOrderId, value); return saveOrderMetadataField(`reg_${vehicleIndex}`, value, normalizedOrderId);
const refreshedOrder = await getOrderDetails(normalizedOrderId);
return refreshedOrder?.[`reg_${vehicleIndex}`] ?? value;
}, },
onSaved: async (value) => { onSaved: async (value) => {
setPersistedRegistrationValue(vehicleIndex, value); setPersistedRegistrationValue(vehicleIndex, value);
@@ -194,6 +199,11 @@ const flushAllRegistrations = async () => {
await Promise.all([1, 2, 3].map((vehicleIndex) => registrationAutosaves[vehicleIndex].flush())); 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 // Function to set a value if it is not null
const createVehicleSelectionFromSearchResult = (value: PosSearchResult) => ({ const createVehicleSelectionFromSearchResult = (value: PosSearchResult) => ({
...value, ...value,
@@ -266,11 +276,12 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
unregisterStepSaveBarrier();
void flushAllRegistrations(); void flushAllRegistrations();
}); });
const closeManualInput = async () => { const closeManualInput = async () => {
await flushAllRegistrations(); await waitForAllRegistrationSaves();
emit("close"); emit("close");
}; };
</script> </script>
@@ -8,9 +8,9 @@ const props = defineProps(['label', 'isText']);
const label = computed(() => props.label || t('admin.pos.buttons.go_back')); const label = computed(() => props.label || t('admin.pos.buttons.go_back'));
// Go to previous step // Go to previous step
const goToPreviousStep = () => { const goToPreviousStep = async () => {
// Get the department id // Get the department id
previousStep(); await previousStep();
} }
const emptyIfText = (string) => props.isText ? '' : string; const emptyIfText = (string) => props.isText ? '' : string;
@@ -32,4 +32,4 @@ const emptyIfText = (string) => props.isText ? '' : string;
<style scoped> <style scoped>
</style> </style>
+201 -59
View File
@@ -52,6 +52,42 @@ export const nextStepDelay = ref(0); // 2 seconds delay
export const isCreatingOrder = ref(false); export const isCreatingOrder = ref(false);
export const desktopStep1PreflightHandler = ref(null); export const desktopStep1PreflightHandler = ref(null);
let createOrderRequest = null; let createOrderRequest = null;
const posStepSaveBarriers = new Set();
let latestOrderDetailsRequestId = 0;
let latestOrderMetadataMutationSequence = 0;
let activeOrderMetadataMutationCount = 0;
export const registerPosStepSaveBarrier = (handler) => {
if (typeof handler !== "function") {
return () => {};
}
posStepSaveBarriers.add(handler);
return () => {
posStepSaveBarriers.delete(handler);
};
};
export const unregisterPosStepSaveBarrier = (handler) => {
posStepSaveBarriers.delete(handler);
};
export const flushPosStepSaveBarriers = async () => {
const barriers = Array.from(posStepSaveBarriers);
if (barriers.length === 0) {
return true;
}
const results = await Promise.allSettled(barriers.map((handler) => handler()));
const failedResult = results.find((result) => result.status === "rejected");
if (failedResult) {
parseError(failedResult.reason, "stepError");
return false;
}
return true;
};
export const setDesktopStep1PreflightHandler = (handler = null) => { export const setDesktopStep1PreflightHandler = (handler = null) => {
desktopStep1PreflightHandler.value = typeof handler === "function" ? handler : null; desktopStep1PreflightHandler.value = typeof handler === "function" ? handler : null;
@@ -109,6 +145,10 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
} }
// Set the delay to 2 seconds // Set the delay to 2 seconds
setNextStepDelay(2); setNextStepDelay(2);
if (!(await flushPosStepSaveBarriers())) {
nextStepDelay.value = 0;
return;
}
// Clear the errors // Clear the errors
clearErrors(); clearErrors();
// If the current step is 1, create the order // If the current step is 1, create the order
@@ -188,8 +228,22 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
step.value++; step.value++;
}; };
export const previousStep = () => { export const previousStep = async () => {
step.value--; if (nextStepDelay.value > 0) {
return;
}
if (!(await flushPosStepSaveBarriers())) {
return;
}
step.value = Math.max(1, step.value - 1);
if (order_id.value) {
pushPosRouteState(`id=${order_id.value}&customer_id=${customer_id.value}&step=${step.value}`);
} else {
pushPosRouteState(`step=${step.value}`);
}
}; };
export const getCurrentStep = () => { export const getCurrentStep = () => {
@@ -784,6 +838,129 @@ watch([reg_1, reg_2, reg_3], () => {
reg_3.value = normalizeRegistrationValue(reg_3.value); reg_3.value = normalizeRegistrationValue(reg_3.value);
}); });
const applyOrderMetadataFieldToPosState = (field, value) => {
if (field === "reference") {
reference.value = normalizeOrderMetadataString(value);
return reference.value;
}
if (field === "notes") {
order_notes.value = normalizeOrderMetadataString(value);
return order_notes.value;
}
if (field === "po") {
order_po.value = normalizeOrderMetadataString(value);
return order_po.value;
}
if (field === "safety_seal") {
order_safety_seal.value = normalizeOrderMetadataString(value);
return order_safety_seal.value;
}
if (field === "reg_1") {
reg_1.value = normalizeRegistrationValue(value);
return reg_1.value;
}
if (field === "reg_2") {
reg_2.value = normalizeRegistrationValue(value);
return reg_2.value;
}
if (field === "reg_3") {
reg_3.value = normalizeRegistrationValue(value);
return reg_3.value;
}
return normalizeOrderMetadataString(value);
};
export const applyOrderDetailsToPosState = (orderData, includes = {}) => {
if (!orderData || typeof orderData !== "object") {
return null;
}
const includedCustomer = includes?.customer ?? null;
const normalizedOrderCustomerNumber = resolveCustomerNumber(orderData?.customer_id);
const selectedCustomerBeforeHydration = getSelectedCustomerNumber();
department_id.value = orderData.department_id;
applyOrderMetadataFieldToPosState("reference", orderData.reference);
applyOrderMetadataFieldToPosState("notes", orderData.notes);
applyOrderMetadataFieldToPosState("po", orderData.po);
applyOrderMetadataFieldToPosState("safety_seal", orderData.safety_seal);
applyOrderMetadataFieldToPosState("reg_1", orderData.reg_1);
applyOrderMetadataFieldToPosState("reg_2", orderData.reg_2);
applyOrderMetadataFieldToPosState("reg_3", orderData.reg_3);
invoiceCollectionId.value = orderData.invoice_collection_id || null;
completed_at.value = orderData.completed_at || null;
if (includedCustomer && typeof includedCustomer === "object") {
const customerSource =
includedCustomer.economic_customer && typeof includedCustomer.economic_customer === "object"
? includedCustomer.economic_customer
: includedCustomer;
selectCustomer(
normalizeCustomerRecord(customerSource, {
...includedCustomer,
customerNumber: normalizedOrderCustomerNumber ?? resolveCustomerNumber(includedCustomer),
name: includedCustomer.name ?? includedCustomer.customer_name ?? orderData.customer_name ?? customer_name.value,
customer_name:
includedCustomer.customer_name ?? includedCustomer.name ?? orderData.customer_name ?? customer_name.value,
})
);
} else if (normalizedOrderCustomerNumber) {
customer_id.value = normalizedOrderCustomerNumber;
if (
selectedCustomerBeforeHydration !== normalizedOrderCustomerNumber ||
customer_name.value === "" ||
!customer_data.value
) {
void searchAndSelectCustomer(normalizedOrderCustomerNumber, { forceRefresh: true });
}
} else {
clearSelectedCustomerState();
}
return orderData;
};
export const saveOrderMetadataField = async (field, value, targetOrderId = order_id.value) => {
const normalizedOrderId = toPositiveInteger(targetOrderId);
const setter = SessionUser.objects.orders.set?.[field];
if (!normalizedOrderId || typeof setter !== "function") {
throw new Error(`Unable to save order field: ${field}`);
}
const mutationSequence = latestOrderMetadataMutationSequence + 1;
latestOrderMetadataMutationSequence = mutationSequence;
activeOrderMetadataMutationCount += 1;
const isRegistrationField = field === "reg_1" || field === "reg_2" || field === "reg_3";
const requestValue = isRegistrationField
? String(value ?? "").toUpperCase()
: normalizeOrderMetadataString(value);
const normalizedValue = isRegistrationField ? normalizeRegistrationValue(value) : requestValue;
try {
const response = await setter(normalizedOrderId, requestValue);
const updatedOrder = response?.data?.data;
if (updatedOrder && toPositiveInteger(updatedOrder.id) === normalizedOrderId) {
if (mutationSequence === latestOrderMetadataMutationSequence && activeOrderMetadataMutationCount === 1) {
applyOrderDetailsToPosState(updatedOrder, response?.data?.includes ?? {});
}
return applyOrderMetadataFieldToPosState(field, updatedOrder[field] ?? normalizedValue);
}
return applyOrderMetadataFieldToPosState(field, normalizedValue);
} finally {
activeOrderMetadataMutationCount = Math.max(0, activeOrderMetadataMutationCount - 1);
}
};
/** Get the order details by order id */ /** Get the order details by order id */
export const getOrderDetails = async (id = null) => { export const getOrderDetails = async (id = null) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@@ -791,6 +968,9 @@ export const getOrderDetails = async (id = null) => {
if (!token || !targetOrderId) { if (!token || !targetOrderId) {
return null; return null;
} }
const requestId = ++latestOrderDetailsRequestId;
const metadataMutationSequenceAtRequest = latestOrderMetadataMutationSequence;
const hadActiveMetadataMutationAtRequest = activeOrderMetadataMutationCount > 0;
return axios return axios
.get(API_URL + "/order?id=" + targetOrderId, { .get(API_URL + "/order?id=" + targetOrderId, {
headers: { headers: {
@@ -799,54 +979,21 @@ export const getOrderDetails = async (id = null) => {
}) })
.then((response) => { .then((response) => {
const order_data = response.data.data; const order_data = response.data.data;
const includedCustomer = response?.data?.includes?.customer ?? null; if (!order_data) {
const normalizedOrderCustomerNumber = resolveCustomerNumber(order_data?.customer_id); return null;
const selectedCustomerBeforeHydration = getSelectedCustomerNumber(); }
// Set the order details
department_id.value = order_data.department_id;
reference.value = order_data.reference || "";
order_notes.value = order_data.notes || "";
order_po.value = order_data.po || "";
order_safety_seal.value = normalizeOrderMetadataString(order_data.safety_seal);
reg_1.value = order_data.reg_1 || "";
reg_2.value = order_data.reg_2 || "";
reg_3.value = order_data.reg_3 || "";
invoiceCollectionId.value = order_data.invoice_collection_id || null;
completed_at.value = order_data.completed_at || null;
if (includedCustomer && typeof includedCustomer === "object") { const normalizedResponseOrderId = toPositiveInteger(order_data.id ?? targetOrderId);
const customerSource = const normalizedCurrentOrderId = toPositiveInteger(order_id.value);
includedCustomer.economic_customer && typeof includedCustomer.economic_customer === "object" const isStaleForDifferentOrder =
? includedCustomer.economic_customer requestId !== latestOrderDetailsRequestId &&
: includedCustomer; normalizedCurrentOrderId &&
normalizedResponseOrderId &&
normalizedCurrentOrderId !== normalizedResponseOrderId;
const isStaleBeforeMetadataMutation = metadataMutationSequenceAtRequest !== latestOrderMetadataMutationSequence;
selectCustomer( if (!isStaleForDifferentOrder && !isStaleBeforeMetadataMutation && !hadActiveMetadataMutationAtRequest) {
normalizeCustomerRecord(customerSource, { applyOrderDetailsToPosState(order_data, response?.data?.includes ?? {});
...includedCustomer,
customerNumber: normalizedOrderCustomerNumber ?? resolveCustomerNumber(includedCustomer),
name:
includedCustomer.name ??
includedCustomer.customer_name ??
order_data.customer_name ??
customer_name.value,
customer_name:
includedCustomer.customer_name ??
includedCustomer.name ??
order_data.customer_name ??
customer_name.value,
})
);
} else if (normalizedOrderCustomerNumber) {
customer_id.value = normalizedOrderCustomerNumber;
if (
selectedCustomerBeforeHydration !== normalizedOrderCustomerNumber ||
customer_name.value === "" ||
!customer_data.value
) {
void searchAndSelectCustomer(normalizedOrderCustomerNumber, { forceRefresh: true });
}
} else {
clearSelectedCustomerState();
} }
// Return the order data // Return the order data
return order_data; return order_data;
@@ -1338,7 +1485,7 @@ const copyLastWashReferenceToEmptyCurrentOrder = async (sourceReference) => {
const normalizedOrderId = toPositiveInteger(order_id.value); const normalizedOrderId = toPositiveInteger(order_id.value);
if (normalizedOrderId) { if (normalizedOrderId) {
await SessionUser.objects.orders.set.reference(normalizedOrderId, normalizedSourceReference); await saveOrderMetadataField("reference", normalizedSourceReference, normalizedOrderId);
} }
}; };
@@ -2208,32 +2355,27 @@ export const hydrateSelectedOrderBookingForDesktop = async () => {
const bookingReference = getOrderBookingReferenceValue(booking); const bookingReference = getOrderBookingReferenceValue(booking);
if (bookingReference !== "" && bookingReference !== reference.value) { if (bookingReference !== "" && bookingReference !== reference.value) {
await SessionUser.objects.orders.set.reference(normalizedOrderId, bookingReference); reference.value = await saveOrderMetadataField("reference", bookingReference, normalizedOrderId);
reference.value = bookingReference;
} }
const bookingReg1 = getOrderBookingReg1Value(booking); const bookingReg1 = getOrderBookingReg1Value(booking);
if (bookingReg1 !== "" && bookingReg1 !== reg_1.value) { if (bookingReg1 !== "" && bookingReg1 !== reg_1.value) {
await SessionUser.objects.orders.set.reg_1(normalizedOrderId, bookingReg1); reg_1.value = await saveOrderMetadataField("reg_1", bookingReg1, normalizedOrderId);
reg_1.value = bookingReg1;
} }
const bookingReg2 = getOrderBookingReg2Value(booking); const bookingReg2 = getOrderBookingReg2Value(booking);
if (bookingReg2 !== reg_2.value) { if (bookingReg2 !== reg_2.value) {
await SessionUser.objects.orders.set.reg_2(normalizedOrderId, bookingReg2); reg_2.value = await saveOrderMetadataField("reg_2", bookingReg2, normalizedOrderId);
reg_2.value = bookingReg2;
} }
const bookingNotes = getOrderBookingNotesValue(booking); const bookingNotes = getOrderBookingNotesValue(booking);
if (bookingNotes !== "" && bookingNotes !== order_notes.value) { if (bookingNotes !== "" && bookingNotes !== order_notes.value) {
await SessionUser.objects.orders.set.notes(normalizedOrderId, bookingNotes); order_notes.value = await saveOrderMetadataField("notes", bookingNotes, normalizedOrderId);
order_notes.value = bookingNotes;
} }
const bookingPo = String(booking?.po ?? "").trim(); const bookingPo = String(booking?.po ?? "").trim();
if (bookingPo !== "" && isBlankPosMetadataValue(order_po.value)) { if (bookingPo !== "" && isBlankPosMetadataValue(order_po.value)) {
await SessionUser.objects.orders.set.po(normalizedOrderId, bookingPo); order_po.value = await saveOrderMetadataField("po", bookingPo, normalizedOrderId);
order_po.value = bookingPo;
} }
const currentOrderItems = await loadOrderItems(); const currentOrderItems = await loadOrderItems();
+76 -32
View File
@@ -23,10 +23,12 @@ export const useOrderMetadataAutosave = ({
const draft = ref(normalizeValue(resolveSourceValue(source))); const draft = ref(normalizeValue(resolveSourceValue(source)));
const lastSavedValue = ref(normalizeValue(resolveSourceValue(source))); const lastSavedValue = ref(normalizeValue(resolveSourceValue(source)));
const isSaving = ref(false); const isSaving = ref(false);
const lastError = ref(null);
const isDirty = computed(() => normalizeValue(draft.value) !== normalizeValue(lastSavedValue.value)); const isDirty = computed(() => normalizeValue(draft.value) !== normalizeValue(lastSavedValue.value));
let timerId = null; let timerId = null;
let saveAgainAfterCurrentRequest = false; let saveAgainAfterCurrentRequest = false;
let activeSavePromise = null;
let nextSequence = 0; let nextSequence = 0;
let lastAppliedSequence = 0; let lastAppliedSequence = 0;
@@ -66,45 +68,57 @@ export const useOrderMetadataAutosave = ({
const sequence = ++nextSequence; const sequence = ++nextSequence;
let saveCompleted = false; let saveCompleted = false;
isSaving.value = true; isSaving.value = true;
lastError.value = null;
try { const run = (async () => {
const result = await saveValue(requestedValue); try {
const savedValue = normalizeValue(result ?? requestedValue); const result = await saveValue(requestedValue);
const savedValue = normalizeValue(result ?? requestedValue);
if (sequence >= lastAppliedSequence) { if (sequence >= lastAppliedSequence) {
lastAppliedSequence = sequence; lastAppliedSequence = sequence;
lastSavedValue.value = savedValue; lastSavedValue.value = savedValue;
if (normalizeValue(draft.value) === requestedValue) { if (normalizeValue(draft.value) === requestedValue) {
draft.value = savedValue; draft.value = savedValue;
}
}
if (typeof onSaved === "function") {
await onSaved(savedValue, requestedValue);
}
saveCompleted = true;
return true;
} catch (error) {
lastError.value = error;
if (typeof onError === "function") {
onError(error);
} else {
console.error(error);
}
return false;
} finally {
isSaving.value = false;
const shouldSaveAgain = saveAgainAfterCurrentRequest;
saveAgainAfterCurrentRequest = false;
if (saveCompleted && (shouldSaveAgain || isDirty.value) && isDirty.value) {
void persist();
} }
} }
})();
if (typeof onSaved === "function") { const trackedSavePromise = run.finally(() => {
await onSaved(savedValue, requestedValue); if (activeSavePromise === trackedSavePromise) {
activeSavePromise = null;
} }
});
saveCompleted = true; activeSavePromise = trackedSavePromise;
return trackedSavePromise;
return true;
} catch (error) {
if (typeof onError === "function") {
onError(error);
} else {
console.error(error);
}
return false;
} finally {
isSaving.value = false;
const shouldSaveAgain = saveAgainAfterCurrentRequest;
saveAgainAfterCurrentRequest = false;
if (saveCompleted && (shouldSaveAgain || isDirty.value) && isDirty.value) {
void persist();
}
}
}; };
const scheduleSave = () => { const scheduleSave = () => {
@@ -123,6 +137,34 @@ export const useOrderMetadataAutosave = ({
return persist(); return persist();
}; };
const flushAndWait = async () => {
clearPendingSave();
while (isSaving.value && activeSavePromise) {
await activeSavePromise;
}
if (!isDirty.value) {
return false;
}
const didSave = await persist();
if (!didSave && isDirty.value) {
throw lastError.value || new Error("Unable to save order metadata");
}
while (isSaving.value && activeSavePromise) {
await activeSavePromise;
}
if (isDirty.value) {
return flushAndWait();
}
return didSave;
};
onBeforeUnmount(() => { onBeforeUnmount(() => {
clearPendingSave(); clearPendingSave();
@@ -139,10 +181,12 @@ export const useOrderMetadataAutosave = ({
return { return {
draft, draft,
lastSavedValue, lastSavedValue,
lastError,
isDirty, isDirty,
isSaving, isSaving,
scheduleSave, scheduleSave,
flush, flush,
flushAndWait,
clearPendingSave, clearPendingSave,
syncFromSource, syncFromSource,
}; };
@@ -50,6 +50,8 @@ function createCardOrderFixture(overrides = {}) {
customer_id: CARD_CUSTOMER_ID, customer_id: CARD_CUSTOMER_ID,
department_id: DEPARTMENT_ID, department_id: DEPARTMENT_ID,
reference: "CARD-REF-54518", reference: "CARD-REF-54518",
po: "",
safety_seal: "",
notes: "Paid at terminal", notes: "Paid at terminal",
reg_1: "ZZ99999", reg_1: "ZZ99999",
reg_2: "", reg_2: "",
@@ -236,6 +238,61 @@ test.describe("POS desktop card payments", () => {
await expect(stepThree.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 }); await expect(stepThree.getByTestId("pos-stripe-create-intent")).toBeVisible({ timeout: 10_000 });
}); });
test("step 2 customer wishes stay visible after moving to card payment step 3", async ({ page }) => {
const stepTwoReference = "CARD-STEP-2-REF";
const stepTwoPo = "PO-CARD-STEP-2";
const fixture = createCardOrderFixture({
ordersById: {
[ORDER_ID]: {
...createCardOrderFixture().ordersById[ORDER_ID],
reference: "",
po: "",
},
},
});
await mockApi(page, {
authenticated: true,
permissions: DESKTOP_POS_PERMISSIONS,
edgeGateways: false,
pos: fixture,
});
await primeSession(page, `desktop-card-step2-${Date.now()}`);
await page.goto(`/admin/${DEPARTMENT_ID}/modules/pos?id=${ORDER_ID}&customer_id=${CARD_CUSTOMER_ID}&step=2`);
const stepTwo = page.getByTestId("pos-step-2");
await expect(stepTwo).toBeVisible({ timeout: 10_000 });
await expect(stepTwo.getByTestId("pos-order-metadata-grid")).toBeVisible({ timeout: 10_000 });
await stepTwo.getByTestId("pos-order-customer-wishes-reference").click();
await stepTwo.getByTestId("pos-order-customer-wishes-reference-input").fill(stepTwoReference);
await stepTwo.getByTestId("pos-order-customer-wishes-po").click();
await stepTwo.getByTestId("pos-order-customer-wishes-po-input").fill(stepTwoPo);
await stepTwo.getByTestId("pos-next-step").click();
const stepThree = page.getByTestId("pos-step-3");
await expect(stepThree).toBeVisible({ timeout: 10_000 });
await expect(stepThree.getByTestId("pos-order-customer-wishes-reference")).toContainText(stepTwoReference, {
timeout: 10_000,
});
await expect(stepThree.getByTestId("pos-order-customer-wishes-po")).toContainText(stepTwoPo, {
timeout: 10_000,
});
await expect.poll(() => fixture.ordersById[ORDER_ID].reference, { timeout: 10_000 }).toBe(stepTwoReference);
await expect.poll(() => fixture.ordersById[ORDER_ID].po, { timeout: 10_000 }).toBe(stepTwoPo);
await page.reload();
const reloadedStepThree = page.getByTestId("pos-step-3");
await expect(reloadedStepThree).toBeVisible({ timeout: 10_000 });
await expect(reloadedStepThree.getByTestId("pos-order-customer-wishes-reference")).toContainText(stepTwoReference, {
timeout: 10_000,
});
await expect(reloadedStepThree.getByTestId("pos-order-customer-wishes-po")).toContainText(stepTwoPo, {
timeout: 10_000,
});
});
test("idle state renders grouped terminal statuses and only keeps ready terminals selectable", async ({ page }) => { test("idle state renders grouped terminal statuses and only keeps ready terminals selectable", async ({ page }) => {
const fixture = createCardOrderFixture({ const fixture = createCardOrderFixture({
readers: [ readers: [
+123
View File
@@ -93,6 +93,8 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
reference: vi.fn(), reference: vi.fn(),
reg_1: vi.fn(), reg_1: vi.fn(),
reg_2: vi.fn(), reg_2: vi.fn(),
reg_3: vi.fn(),
safety_seal: vi.fn(),
}, },
}, },
order_bookings: { order_bookings: {
@@ -152,6 +154,8 @@ import {
getVehiclePlateBookings, getVehiclePlateBookings,
invoiceCollectionId, invoiceCollectionId,
loadOrderItems, loadOrderItems,
nextStep,
nextStepDelay,
order_id, order_id,
order_items, order_items,
hydrateSelectedOrderBookingForDesktop, hydrateSelectedOrderBookingForDesktop,
@@ -163,7 +167,10 @@ import {
reg_2, reg_2,
reg_3, reg_3,
pendingBookings, pendingBookings,
previousStep,
registerPosStepSaveBarrier,
restoreStoredPosOrderId, restoreStoredPosOrderId,
saveOrderMetadataField,
searchAndSelectCustomer, searchAndSelectCustomer,
setOrderId, setOrderId,
scan_data, scan_data,
@@ -172,6 +179,122 @@ import {
step, step,
} from "@/components/shop/POSDepartmentProcess.vue"; } from "@/components/shop/POSDepartmentProcess.vue";
describe("POSDepartmentProcess order metadata persistence", () => {
beforeEach(() => {
window.history.replaceState({}, "", "/admin/12/modules/pos?step=2");
clearActivePosOrderContext({ clearStep: true });
localStorage.setItem("token", "test-token");
nextStepDelay.value = 0;
order_id.value = 54518;
customer_id.value = 999;
customer_name.value = "Card Terminal Customer";
customer_data.value = {};
department_id.value = 12;
step.value = 2;
Object.values(SessionUser.objects.orders.set).forEach((setter) => setter.mockReset?.());
});
it("applies saved metadata responses to shared POS state", async () => {
SessionUser.objects.orders.set.reference.mockResolvedValueOnce({
data: {
data: {
id: 54518,
customer_id: 999,
department_id: 12,
reference: "STEP-2-REF",
notes: "Visible note",
po: "PO-STEP-2",
safety_seal: "SEAL-1",
reg_1: "aa 12345",
reg_2: "",
reg_3: "",
invoice_collection_id: 200,
completed_at: null,
},
},
});
await expect(saveOrderMetadataField("reference", "STEP-2-REF", 54518)).resolves.toBe("STEP-2-REF");
expect(SessionUser.objects.orders.set.reference).toHaveBeenCalledWith(54518, "STEP-2-REF");
expect(reference.value).toBe("STEP-2-REF");
expect(order_notes.value).toBe("Visible note");
expect(order_po.value).toBe("PO-STEP-2");
expect(order_safety_seal.value).toBe("SEAL-1");
expect(reg_1.value).toBe("AA12345");
});
it("preserves registration input formatting for the save request and normalizes shared state", async () => {
SessionUser.objects.orders.set.reg_1.mockResolvedValueOnce({
data: {
data: {
id: 54518,
reg_1: "CD1234",
},
},
});
await expect(saveOrderMetadataField("reg_1", "cd-12 34", 54518)).resolves.toBe("CD1234");
expect(SessionUser.objects.orders.set.reg_1).toHaveBeenCalledWith(54518, "CD-12 34");
expect(reg_1.value).toBe("CD1234");
});
it("waits for registered field saves before moving from step 2 to step 3", async () => {
let resolveBarrier;
const barrier = vi.fn(
() =>
new Promise((resolve) => {
resolveBarrier = () => {
reference.value = "BARRIER-SAVED";
resolve();
};
})
);
const unregister = registerPosStepSaveBarrier(barrier);
try {
const moveToNextStep = nextStep({ isMobile: false, orderCreation: false });
await Promise.resolve();
expect(barrier).toHaveBeenCalledTimes(1);
expect(window.location.search).toBe("?step=2");
resolveBarrier();
await moveToNextStep;
expect(reference.value).toBe("BARRIER-SAVED");
expect(window.location.search).toContain("step=3");
expect(window.location.search).toContain("id=54518");
expect(window.location.search).toContain("customer_id=999");
} finally {
unregister();
nextStepDelay.value = 0;
}
});
it("waits for registered field saves before moving backward", async () => {
step.value = 3;
window.history.replaceState({}, "", "/admin/12/modules/pos?id=54518&customer_id=999&step=3");
const barrier = vi.fn(async () => {
order_po.value = "PO-BACK-SAVED";
});
const unregister = registerPosStepSaveBarrier(barrier);
try {
await previousStep();
expect(barrier).toHaveBeenCalledTimes(1);
expect(order_po.value).toBe("PO-BACK-SAVED");
expect(step.value).toBe(2);
expect(window.location.search).toContain("step=2");
} finally {
unregister();
nextStepDelay.value = 0;
}
});
});
describe("POSDepartmentProcess.ensureVehiclePlateBookingsLoaded", () => { describe("POSDepartmentProcess.ensureVehiclePlateBookingsLoaded", () => {
beforeEach(() => { beforeEach(() => {
department_id.value = 531; department_id.value = 531;
@@ -85,4 +85,59 @@ describe("useOrderMetadataAutosave", () => {
await Promise.resolve(); await Promise.resolve();
wrapper.unmount(); wrapper.unmount();
}); });
it("flushAndWait waits for an in-flight save and persists the queued latest draft", async () => {
let resolveFirstSave;
const saveValue = vi.fn((value) => {
if (value === "first") {
return new Promise((resolve) => {
resolveFirstSave = () => resolve(value);
});
}
return Promise.resolve(value);
});
const { autosave, wrapper } = mountAutosave({ saveValue });
autosave.draft.value = "first";
const firstFlush = autosave.flush();
autosave.draft.value = "second";
const waitForSaves = autosave.flushAndWait();
await Promise.resolve();
expect(saveValue).toHaveBeenCalledTimes(1);
expect(saveValue).toHaveBeenNthCalledWith(1, "first");
resolveFirstSave();
await expect(firstFlush).resolves.toBe(true);
await expect(waitForSaves).resolves.toBe(false);
expect(saveValue).toHaveBeenCalledTimes(2);
expect(saveValue).toHaveBeenNthCalledWith(2, "second");
expect(autosave.lastSavedValue.value).toBe("second");
expect(autosave.isDirty.value).toBe(false);
wrapper.unmount();
});
it("flushAndWait rejects when the dirty draft cannot be saved", async () => {
const saveError = new Error("server rejected metadata");
const saveValue = vi.fn(async () => {
throw saveError;
});
const { autosave, onError, wrapper } = mountAutosave({ saveValue });
autosave.draft.value = "rejected";
await expect(autosave.flushAndWait()).rejects.toThrow("server rejected metadata");
expect(saveValue).toHaveBeenCalledTimes(1);
expect(onError).toHaveBeenCalledWith(saveError);
expect(autosave.lastError.value).toBe(saveError);
expect(autosave.isDirty.value).toBe(true);
autosave.syncFromSource("rejected");
wrapper.unmount();
});
}); });