Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6463a3d211 | ||
|
|
e792f5820d |
@@ -1,9 +1,12 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
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({
|
||||
order_id: {
|
||||
@@ -123,7 +126,7 @@ const referenceField = createCustomerWishField({
|
||||
key: "reference",
|
||||
label: computed(() => t("common.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",
|
||||
isRequired: () => props.referenceRequired,
|
||||
warningStateWhenEmpty: "danger",
|
||||
@@ -134,7 +137,7 @@ const poField = createCustomerWishField({
|
||||
key: "po",
|
||||
label: computed(() => t("admin.pos.po_number")),
|
||||
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",
|
||||
isRequired: () => props.poRequired,
|
||||
warningStateWhenEmpty: "warning",
|
||||
@@ -145,7 +148,7 @@ const safetySealField = createCustomerWishField({
|
||||
key: "safety_seal",
|
||||
label: computed(() => t("admin.pos.safety_seal")),
|
||||
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",
|
||||
isRequired: () => false,
|
||||
warningStateWhenEmpty: null,
|
||||
@@ -161,6 +164,15 @@ const safetySealField = createCustomerWishField({
|
||||
const fields = computed(() => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
|
||||
import {
|
||||
registerPosStepSaveBarrier,
|
||||
saveOrderMetadataField,
|
||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||
|
||||
const props = defineProps({
|
||||
order_id: {
|
||||
@@ -30,8 +33,7 @@ const isEditing = ref(false);
|
||||
const autosave = useOrderMetadataAutosave({
|
||||
source: () => props.note,
|
||||
saveValue: async (value) => {
|
||||
await SessionUser.objects.orders.set.notes(props.order_id, value);
|
||||
return value;
|
||||
return saveOrderMetadataField("notes", value, props.order_id);
|
||||
},
|
||||
onSaved: async (savedValue, requestedValue) => {
|
||||
if (typeof props.onSaved === "function") {
|
||||
@@ -68,6 +70,12 @@ const lostFocus = async () => {
|
||||
isEditing.value = false;
|
||||
await autosave.flush();
|
||||
};
|
||||
|
||||
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(() => autosave.flushAndWait());
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unregisterStepSaveBarrier();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, ref, watch } from "vue";
|
||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
|
||||
import {
|
||||
registerPosStepSaveBarrier,
|
||||
saveOrderMetadataField,
|
||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||
|
||||
const props = defineProps({
|
||||
index: {
|
||||
@@ -36,8 +39,7 @@ autosave = useOrderMetadataAutosave({
|
||||
source: () => props.modelValue,
|
||||
normalizeValue: normalizeRegistrationDraft,
|
||||
saveValue: async (value) => {
|
||||
await SessionUser.objects.orders.set[fieldKey.value](props.order_id, value);
|
||||
return value;
|
||||
return saveOrderMetadataField(fieldKey.value, value, props.order_id);
|
||||
},
|
||||
onSaved: async () => {
|
||||
await props.loadOrder();
|
||||
@@ -74,6 +76,12 @@ const closeEditor = async () => {
|
||||
isEditing.value = false;
|
||||
await autosave.flush();
|
||||
};
|
||||
|
||||
const unregisterStepSaveBarrier = registerPosStepSaveBarrier(() => autosave.flushAndWait());
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unregisterStepSaveBarrier();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
setBackgroundColor,
|
||||
backgroundColors,
|
||||
} 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 PosDepartmentStepMobile2Product from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2Product.vue";
|
||||
import PosDepartmentStepMobile2LastOrder from "@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile2LastOrder.vue";
|
||||
@@ -43,6 +43,8 @@ import {
|
||||
customer_name,
|
||||
isAddonRestricted,
|
||||
canBuyAdditionalServices,
|
||||
registerPosStepSaveBarrier,
|
||||
saveOrderMetadataField,
|
||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { createOrderItem, getOrderItems, removeOrderItem } from "@/components/shop/OrdersItems.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
|
||||
if (booking.po && typeof booking.po === "string" && booking.po.trim() !== "") {
|
||||
try {
|
||||
await SessionUser.objects.orders.set.po(order_id.value, booking.po);
|
||||
order_po.value = booking.po.trim();
|
||||
order_po.value = await saveOrderMetadataField("po", booking.po, order_id.value);
|
||||
} catch (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();
|
||||
if (bookingReference !== "") {
|
||||
try {
|
||||
await SessionUser.objects.orders.set.reference(order_id.value, bookingReference);
|
||||
persistedReference.value = bookingReference;
|
||||
metadata.setReference(bookingReference);
|
||||
const savedReference = await saveOrderMetadataField("reference", bookingReference, order_id.value);
|
||||
persistedReference.value = savedReference;
|
||||
metadata.setReference(savedReference);
|
||||
hydrateReferenceFromSources();
|
||||
} catch (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();
|
||||
if (bookingNotes !== "") {
|
||||
try {
|
||||
await SessionUser.objects.orders.set.notes(order_id.value, bookingNotes);
|
||||
order_notes.value = bookingNotes;
|
||||
notes.value = bookingNotes;
|
||||
metadata.setNotes(bookingNotes);
|
||||
const savedNotes = await saveOrderMetadataField("notes", bookingNotes, order_id.value);
|
||||
order_notes.value = savedNotes;
|
||||
notes.value = savedNotes;
|
||||
metadata.setNotes(savedNotes);
|
||||
} catch (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);
|
||||
|
||||
if (bookingReg1 && bookingReg1 !== currentReg1) {
|
||||
reg_1.value = bookingReg1;
|
||||
await SessionUser.objects.orders.set.reg_1(order_id.value, bookingReg1);
|
||||
reg_1.value = await saveOrderMetadataField("reg_1", bookingReg1, order_id.value);
|
||||
}
|
||||
|
||||
if (bookingReg2 !== currentReg2) {
|
||||
reg_2.value = bookingReg2;
|
||||
await SessionUser.objects.orders.set.reg_2(order_id.value, bookingReg2);
|
||||
reg_2.value = await saveOrderMetadataField("reg_2", bookingReg2, order_id.value);
|
||||
}
|
||||
|
||||
const bookingMatches = Array.isArray(vehicles.vehicle_1.value?.booking_matches)
|
||||
@@ -538,8 +537,7 @@ const notesAutosave = useOrderMetadataAutosave({
|
||||
return value;
|
||||
}
|
||||
|
||||
await SessionUser.objects.orders.set.notes(normalizedOrderId, value);
|
||||
return value;
|
||||
return saveOrderMetadataField("notes", value, normalizedOrderId);
|
||||
},
|
||||
onSaved: async (value) => {
|
||||
order_notes.value = value;
|
||||
@@ -555,8 +553,7 @@ const referenceAutosave = useOrderMetadataAutosave({
|
||||
return value;
|
||||
}
|
||||
|
||||
await SessionUser.objects.orders.set.reference(normalizedOrderId, value);
|
||||
return value;
|
||||
return saveOrderMetadataField("reference", value, normalizedOrderId);
|
||||
},
|
||||
onSaved: async (value) => {
|
||||
persistedReference.value = value;
|
||||
@@ -572,8 +569,7 @@ const safetySealAutosave = useOrderMetadataAutosave({
|
||||
return value;
|
||||
}
|
||||
|
||||
await SessionUser.objects.orders.set.safety_seal(normalizedOrderId, value);
|
||||
return value;
|
||||
return saveOrderMetadataField("safety_seal", value, normalizedOrderId);
|
||||
},
|
||||
onSaved: async (value) => {
|
||||
order_safety_seal.value = value;
|
||||
@@ -585,6 +581,18 @@ const stepTwoNotesInput = notesAutosave.draft;
|
||||
const stepTwoReferenceInput = referenceAutosave.draft;
|
||||
const stepTwoSafetySealInput = safetySealAutosave.draft;
|
||||
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 orderReference = normalizeReferenceValue(persistedReference.value);
|
||||
|
||||
+35
-19
@@ -20,6 +20,7 @@ import {
|
||||
hasAttribute,
|
||||
uploadAttachment,
|
||||
restoreStoredPosOrderId,
|
||||
saveOrderMetadataField,
|
||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import * as POSDepartmentProcess from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { errors } from "@/components/request/HandleGlobalError.vue";
|
||||
@@ -96,17 +97,30 @@ const props = defineProps({
|
||||
},
|
||||
});
|
||||
|
||||
const updateReference = () => {
|
||||
if (!!order_id.value && order_id.value > 0) SessionUser.objects.orders.set.reference(order_id.value, reference.value);
|
||||
const getActiveOrderId = () => {
|
||||
const parsedOrderId = Number.parseInt(String(order_id.value ?? ""), 10);
|
||||
return Number.isInteger(parsedOrderId) && parsedOrderId > 0 ? parsedOrderId : null;
|
||||
};
|
||||
|
||||
const updateNotes = () => {
|
||||
if (!!order_id.value && order_id.value > 0) SessionUser.objects.orders.set.notes(order_id.value, order_notes.value);
|
||||
const updateReference = async () => {
|
||||
const activeOrderId = getActiveOrderId();
|
||||
if (activeOrderId) {
|
||||
await saveOrderMetadataField("reference", reference.value, activeOrderId);
|
||||
}
|
||||
};
|
||||
|
||||
const updateSafetySeal = () => {
|
||||
if (!!order_id.value && order_id.value > 0)
|
||||
SessionUser.objects.orders.set.safety_seal(order_id.value, order_safety_seal.value);
|
||||
const updateNotes = async () => {
|
||||
const activeOrderId = getActiveOrderId();
|
||||
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
|
||||
@@ -196,9 +210,9 @@ const step1 = async () => {
|
||||
// Reset the vehicle selection
|
||||
views.vehicleSelection.value = false;
|
||||
// Merge references and notes
|
||||
mergeReferences();
|
||||
mergeNotes();
|
||||
mergeSafetySeal();
|
||||
await mergeReferences();
|
||||
await mergeNotes();
|
||||
await mergeSafetySeal();
|
||||
// We only need to check if the customer requires reference, if the field is empty.
|
||||
if (!isNonEmptyString(reference.value)) {
|
||||
const isValid = await validateReferenceRequirements();
|
||||
@@ -214,37 +228,39 @@ const mergeReferences = () => {
|
||||
if (metadata.getReference() && metadata.getReference().length >= 1) {
|
||||
// If there is no reference, set it to the registration number of vehicle 1
|
||||
reference.value = metadata.getReference();
|
||||
updateReference();
|
||||
return updateReference();
|
||||
} else if (reference.value && reference.value.length >= 1) {
|
||||
// If there is a reference in the field, set it to the metadata
|
||||
metadata.setReference(reference.value);
|
||||
updateReference();
|
||||
return updateReference();
|
||||
} 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
|
||||
reference.value = vehicles.vehicle_1.value?.reference;
|
||||
metadata.setReference(reference.value);
|
||||
updateReference();
|
||||
return updateReference();
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
const mergeNotes = () => {
|
||||
if (metadata.getNotes() && metadata.getNotes().length >= 1) {
|
||||
order_notes.value = metadata.getNotes();
|
||||
updateNotes();
|
||||
return updateNotes();
|
||||
} else if (order_notes.value && order_notes.value.length >= 1) {
|
||||
metadata.setNotes(order_notes.value);
|
||||
updateNotes();
|
||||
return updateNotes();
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
const mergeSafetySeal = () => {
|
||||
const resolvedSafetySeal = getResolvedMobileSafetySeal();
|
||||
order_safety_seal.value = resolvedSafetySeal;
|
||||
syncMobileSafetySealState(resolvedSafetySeal);
|
||||
updateSafetySeal();
|
||||
return updateSafetySeal();
|
||||
};
|
||||
const finalizeStep1 = async () => {
|
||||
mergeReferences();
|
||||
mergeNotes();
|
||||
mergeSafetySeal();
|
||||
await mergeReferences();
|
||||
await mergeNotes();
|
||||
await mergeSafetySeal();
|
||||
// Upload the attachments if there are any (And the order id has been created)
|
||||
if (attachments.getBase64().length > 0 && order_id.value > 0) {
|
||||
const uploadResults = await Promise.all(
|
||||
|
||||
+4
-3
@@ -5,6 +5,7 @@ import {
|
||||
order_safety_seal,
|
||||
reference,
|
||||
clearStoredPosOrderId,
|
||||
saveOrderMetadataField,
|
||||
} from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { metadata } from "./PosDepartmentStepMobileFlow.vue";
|
||||
|
||||
@@ -52,9 +53,9 @@ export const persistCurrentMobileOrderMetadata = async () => {
|
||||
order_notes.value = normalizedNotes;
|
||||
|
||||
await Promise.all([
|
||||
SessionUser.objects.orders.set.reference(normalizedOrderId, normalizedReference),
|
||||
SessionUser.objects.orders.set.notes(normalizedOrderId, normalizedNotes),
|
||||
SessionUser.objects.orders.set.safety_seal(normalizedOrderId, normalizedSafetySeal),
|
||||
saveOrderMetadataField("reference", normalizedReference, normalizedOrderId),
|
||||
saveOrderMetadataField("notes", normalizedNotes, normalizedOrderId),
|
||||
saveOrderMetadataField("safety_seal", normalizedSafetySeal, normalizedOrderId),
|
||||
]);
|
||||
|
||||
return normalizedOrderId;
|
||||
|
||||
+16
-5
@@ -1,6 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
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 BarcodeScanner from "@/components/viewport/elements/icons/BarcodeScanner.vue";
|
||||
import GenericButton from "@/components/viewport/page/templates/generic/graphics/GenericButton.vue";
|
||||
@@ -103,9 +110,7 @@ const createRegistrationAutosave = (vehicleIndex: number) =>
|
||||
return value;
|
||||
}
|
||||
|
||||
await SessionUser.objects.orders.set[`reg_${vehicleIndex}`](normalizedOrderId, value);
|
||||
const refreshedOrder = await getOrderDetails(normalizedOrderId);
|
||||
return refreshedOrder?.[`reg_${vehicleIndex}`] ?? value;
|
||||
return saveOrderMetadataField(`reg_${vehicleIndex}`, value, normalizedOrderId);
|
||||
},
|
||||
onSaved: async (value) => {
|
||||
setPersistedRegistrationValue(vehicleIndex, value);
|
||||
@@ -194,6 +199,11 @@ 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,
|
||||
@@ -266,11 +276,12 @@ onMounted(() => {
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unregisterStepSaveBarrier();
|
||||
void flushAllRegistrations();
|
||||
});
|
||||
|
||||
const closeManualInput = async () => {
|
||||
await flushAllRegistrations();
|
||||
await waitForAllRegistrationSaves();
|
||||
emit("close");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,9 +8,9 @@ const props = defineProps(['label', 'isText']);
|
||||
const label = computed(() => props.label || t('admin.pos.buttons.go_back'));
|
||||
|
||||
// Go to previous step
|
||||
const goToPreviousStep = () => {
|
||||
const goToPreviousStep = async () => {
|
||||
// Get the department id
|
||||
previousStep();
|
||||
await previousStep();
|
||||
}
|
||||
|
||||
const emptyIfText = (string) => props.isText ? '' : string;
|
||||
|
||||
@@ -52,6 +52,42 @@ export const nextStepDelay = ref(0); // 2 seconds delay
|
||||
export const isCreatingOrder = ref(false);
|
||||
export const desktopStep1PreflightHandler = ref(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) => {
|
||||
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
|
||||
setNextStepDelay(2);
|
||||
if (!(await flushPosStepSaveBarriers())) {
|
||||
nextStepDelay.value = 0;
|
||||
return;
|
||||
}
|
||||
// Clear the errors
|
||||
clearErrors();
|
||||
// If the current step is 1, create the order
|
||||
@@ -188,8 +228,22 @@ export const nextStep = async (options = { isMobile: false, orderCreation: true
|
||||
step.value++;
|
||||
};
|
||||
|
||||
export const previousStep = () => {
|
||||
step.value--;
|
||||
export const previousStep = async () => {
|
||||
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 = () => {
|
||||
@@ -784,6 +838,129 @@ watch([reg_1, reg_2, reg_3], () => {
|
||||
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 */
|
||||
export const getOrderDetails = async (id = null) => {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -791,6 +968,9 @@ export const getOrderDetails = async (id = null) => {
|
||||
if (!token || !targetOrderId) {
|
||||
return null;
|
||||
}
|
||||
const requestId = ++latestOrderDetailsRequestId;
|
||||
const metadataMutationSequenceAtRequest = latestOrderMetadataMutationSequence;
|
||||
const hadActiveMetadataMutationAtRequest = activeOrderMetadataMutationCount > 0;
|
||||
return axios
|
||||
.get(API_URL + "/order?id=" + targetOrderId, {
|
||||
headers: {
|
||||
@@ -799,54 +979,21 @@ export const getOrderDetails = async (id = null) => {
|
||||
})
|
||||
.then((response) => {
|
||||
const order_data = response.data.data;
|
||||
const includedCustomer = response?.data?.includes?.customer ?? null;
|
||||
const normalizedOrderCustomerNumber = resolveCustomerNumber(order_data?.customer_id);
|
||||
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 (!order_data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (includedCustomer && typeof includedCustomer === "object") {
|
||||
const customerSource =
|
||||
includedCustomer.economic_customer && typeof includedCustomer.economic_customer === "object"
|
||||
? includedCustomer.economic_customer
|
||||
: includedCustomer;
|
||||
const normalizedResponseOrderId = toPositiveInteger(order_data.id ?? targetOrderId);
|
||||
const normalizedCurrentOrderId = toPositiveInteger(order_id.value);
|
||||
const isStaleForDifferentOrder =
|
||||
requestId !== latestOrderDetailsRequestId &&
|
||||
normalizedCurrentOrderId &&
|
||||
normalizedResponseOrderId &&
|
||||
normalizedCurrentOrderId !== normalizedResponseOrderId;
|
||||
const isStaleBeforeMetadataMutation = metadataMutationSequenceAtRequest !== latestOrderMetadataMutationSequence;
|
||||
|
||||
selectCustomer(
|
||||
normalizeCustomerRecord(customerSource, {
|
||||
...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();
|
||||
if (!isStaleForDifferentOrder && !isStaleBeforeMetadataMutation && !hadActiveMetadataMutationAtRequest) {
|
||||
applyOrderDetailsToPosState(order_data, response?.data?.includes ?? {});
|
||||
}
|
||||
// Return the order data
|
||||
return order_data;
|
||||
@@ -1338,7 +1485,7 @@ const copyLastWashReferenceToEmptyCurrentOrder = async (sourceReference) => {
|
||||
|
||||
const normalizedOrderId = toPositiveInteger(order_id.value);
|
||||
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);
|
||||
if (bookingReference !== "" && bookingReference !== reference.value) {
|
||||
await SessionUser.objects.orders.set.reference(normalizedOrderId, bookingReference);
|
||||
reference.value = bookingReference;
|
||||
reference.value = await saveOrderMetadataField("reference", bookingReference, normalizedOrderId);
|
||||
}
|
||||
|
||||
const bookingReg1 = getOrderBookingReg1Value(booking);
|
||||
if (bookingReg1 !== "" && bookingReg1 !== reg_1.value) {
|
||||
await SessionUser.objects.orders.set.reg_1(normalizedOrderId, bookingReg1);
|
||||
reg_1.value = bookingReg1;
|
||||
reg_1.value = await saveOrderMetadataField("reg_1", bookingReg1, normalizedOrderId);
|
||||
}
|
||||
|
||||
const bookingReg2 = getOrderBookingReg2Value(booking);
|
||||
if (bookingReg2 !== reg_2.value) {
|
||||
await SessionUser.objects.orders.set.reg_2(normalizedOrderId, bookingReg2);
|
||||
reg_2.value = bookingReg2;
|
||||
reg_2.value = await saveOrderMetadataField("reg_2", bookingReg2, normalizedOrderId);
|
||||
}
|
||||
|
||||
const bookingNotes = getOrderBookingNotesValue(booking);
|
||||
if (bookingNotes !== "" && bookingNotes !== order_notes.value) {
|
||||
await SessionUser.objects.orders.set.notes(normalizedOrderId, bookingNotes);
|
||||
order_notes.value = bookingNotes;
|
||||
order_notes.value = await saveOrderMetadataField("notes", bookingNotes, normalizedOrderId);
|
||||
}
|
||||
|
||||
const bookingPo = String(booking?.po ?? "").trim();
|
||||
if (bookingPo !== "" && isBlankPosMetadataValue(order_po.value)) {
|
||||
await SessionUser.objects.orders.set.po(normalizedOrderId, bookingPo);
|
||||
order_po.value = bookingPo;
|
||||
order_po.value = await saveOrderMetadataField("po", bookingPo, normalizedOrderId);
|
||||
}
|
||||
|
||||
const currentOrderItems = await loadOrderItems();
|
||||
|
||||
@@ -23,10 +23,12 @@ export const useOrderMetadataAutosave = ({
|
||||
const draft = ref(normalizeValue(resolveSourceValue(source)));
|
||||
const lastSavedValue = ref(normalizeValue(resolveSourceValue(source)));
|
||||
const isSaving = ref(false);
|
||||
const lastError = ref(null);
|
||||
const isDirty = computed(() => normalizeValue(draft.value) !== normalizeValue(lastSavedValue.value));
|
||||
|
||||
let timerId = null;
|
||||
let saveAgainAfterCurrentRequest = false;
|
||||
let activeSavePromise = null;
|
||||
let nextSequence = 0;
|
||||
let lastAppliedSequence = 0;
|
||||
|
||||
@@ -66,45 +68,57 @@ export const useOrderMetadataAutosave = ({
|
||||
const sequence = ++nextSequence;
|
||||
let saveCompleted = false;
|
||||
isSaving.value = true;
|
||||
lastError.value = null;
|
||||
|
||||
try {
|
||||
const result = await saveValue(requestedValue);
|
||||
const savedValue = normalizeValue(result ?? requestedValue);
|
||||
const run = (async () => {
|
||||
try {
|
||||
const result = await saveValue(requestedValue);
|
||||
const savedValue = normalizeValue(result ?? requestedValue);
|
||||
|
||||
if (sequence >= lastAppliedSequence) {
|
||||
lastAppliedSequence = sequence;
|
||||
lastSavedValue.value = savedValue;
|
||||
if (sequence >= lastAppliedSequence) {
|
||||
lastAppliedSequence = sequence;
|
||||
lastSavedValue.value = savedValue;
|
||||
|
||||
if (normalizeValue(draft.value) === requestedValue) {
|
||||
draft.value = savedValue;
|
||||
if (normalizeValue(draft.value) === requestedValue) {
|
||||
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") {
|
||||
await onSaved(savedValue, requestedValue);
|
||||
const trackedSavePromise = run.finally(() => {
|
||||
if (activeSavePromise === trackedSavePromise) {
|
||||
activeSavePromise = null;
|
||||
}
|
||||
|
||||
saveCompleted = true;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
activeSavePromise = trackedSavePromise;
|
||||
return trackedSavePromise;
|
||||
};
|
||||
|
||||
const scheduleSave = () => {
|
||||
@@ -123,6 +137,34 @@ export const useOrderMetadataAutosave = ({
|
||||
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(() => {
|
||||
clearPendingSave();
|
||||
|
||||
@@ -139,10 +181,12 @@ export const useOrderMetadataAutosave = ({
|
||||
return {
|
||||
draft,
|
||||
lastSavedValue,
|
||||
lastError,
|
||||
isDirty,
|
||||
isSaving,
|
||||
scheduleSave,
|
||||
flush,
|
||||
flushAndWait,
|
||||
clearPendingSave,
|
||||
syncFromSource,
|
||||
};
|
||||
|
||||
@@ -50,6 +50,8 @@ function createCardOrderFixture(overrides = {}) {
|
||||
customer_id: CARD_CUSTOMER_ID,
|
||||
department_id: DEPARTMENT_ID,
|
||||
reference: "CARD-REF-54518",
|
||||
po: "",
|
||||
safety_seal: "",
|
||||
notes: "Paid at terminal",
|
||||
reg_1: "ZZ99999",
|
||||
reg_2: "",
|
||||
@@ -236,6 +238,61 @@ test.describe("POS desktop card payments", () => {
|
||||
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 }) => {
|
||||
const fixture = createCardOrderFixture({
|
||||
readers: [
|
||||
|
||||
@@ -93,6 +93,8 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
|
||||
reference: vi.fn(),
|
||||
reg_1: vi.fn(),
|
||||
reg_2: vi.fn(),
|
||||
reg_3: vi.fn(),
|
||||
safety_seal: vi.fn(),
|
||||
},
|
||||
},
|
||||
order_bookings: {
|
||||
@@ -152,6 +154,8 @@ import {
|
||||
getVehiclePlateBookings,
|
||||
invoiceCollectionId,
|
||||
loadOrderItems,
|
||||
nextStep,
|
||||
nextStepDelay,
|
||||
order_id,
|
||||
order_items,
|
||||
hydrateSelectedOrderBookingForDesktop,
|
||||
@@ -163,7 +167,10 @@ import {
|
||||
reg_2,
|
||||
reg_3,
|
||||
pendingBookings,
|
||||
previousStep,
|
||||
registerPosStepSaveBarrier,
|
||||
restoreStoredPosOrderId,
|
||||
saveOrderMetadataField,
|
||||
searchAndSelectCustomer,
|
||||
setOrderId,
|
||||
scan_data,
|
||||
@@ -172,6 +179,122 @@ import {
|
||||
step,
|
||||
} 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", () => {
|
||||
beforeEach(() => {
|
||||
department_id.value = 531;
|
||||
|
||||
@@ -85,4 +85,59 @@ describe("useOrderMetadataAutosave", () => {
|
||||
await Promise.resolve();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user