Collect audit reason for extra time sales (#240)
## Summary - Add a shared POS audit helper for approved 10-minute extra sale reason/comment payloads. - Prompt for audit metadata in desktop add/copy, desktop item edit, booking hydration, and mobile completion rebuild flows. - Include preview evidence files under `docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/`. ## Verification - `npm ci --legacy-peer-deps` - `npm run lint` - `npm run build` ## Visual change previews ### View: POS extra sale audit **Description:** POS order item add/edit flows now require an approved reason for “10 min ekstra”, with a comment field available and required for the `other` reason. #### Mobile (390x844) **Before:**  **After:**  #### Tablet (768x1024) **Before:**  **After:**  #### Desktop (1440x900) **Before:**  **After:**  ## Notes - Automatic merge remains disabled per Workboard contract. --------- Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk> Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
co-authored by
Jeppe Bundgaard
openhands
parent
eb8482585b
commit
b1e0c61df0
BIN
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"kind": "VisualEvidenceManifestV1",
|
||||
"taskId": "workboard-94209138-31f6-422e-ac8c-181ad391b8a7",
|
||||
"view": "POS extra sale audit",
|
||||
"files": [
|
||||
{
|
||||
"device": "mobile",
|
||||
"state": "before",
|
||||
"path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-before.png",
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"device": "mobile",
|
||||
"state": "after",
|
||||
"path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-mobile-after.png",
|
||||
"width": 390,
|
||||
"height": 844
|
||||
},
|
||||
{
|
||||
"device": "tablet",
|
||||
"state": "before",
|
||||
"path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-before.png",
|
||||
"width": 768,
|
||||
"height": 1024
|
||||
},
|
||||
{
|
||||
"device": "tablet",
|
||||
"state": "after",
|
||||
"path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-tablet-after.png",
|
||||
"width": 768,
|
||||
"height": 1024
|
||||
},
|
||||
{
|
||||
"device": "desktop",
|
||||
"state": "before",
|
||||
"path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-before.png",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
},
|
||||
{
|
||||
"device": "desktop",
|
||||
"state": "after",
|
||||
"path": "docs/pr-previews/workboard-94209138-31f6-422e-ac8c-181ad391b8a7/pos-extra-sale-audit-desktop-after.png",
|
||||
"width": 1440,
|
||||
"height": 900
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,6 +3,11 @@ import { computed, reactive, ref, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { buildAuditedOrderItemReasonPayload, editOrderItem } from "@/components/shop/OrdersItems.vue";
|
||||
import {
|
||||
EXTRA_SALE_REASONS,
|
||||
isExtraSaleAuditProduct,
|
||||
isExtraSaleCommentRequired,
|
||||
} from "@/components/displays/department/pos/utils/extraSaleAudit.js";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
@@ -29,6 +34,8 @@ const form = reactive({
|
||||
notes: '',
|
||||
reference: '',
|
||||
quantity: '1',
|
||||
extraSaleReasonCode: '',
|
||||
extraSaleComment: '',
|
||||
});
|
||||
|
||||
const isSubmitting = ref(false);
|
||||
@@ -45,6 +52,8 @@ const syncForm = () => {
|
||||
form.notes = String(props.orderItem?.notes ?? '');
|
||||
form.reference = String(props.orderItem?.reference ?? '');
|
||||
form.quantity = String(props.orderItem?.quantity ?? 1);
|
||||
form.extraSaleReasonCode = String(props.orderItem?.extra_sale_reason_code ?? '');
|
||||
form.extraSaleComment = String(props.orderItem?.extra_sale_comment ?? '');
|
||||
errorMessage.value = '';
|
||||
};
|
||||
|
||||
@@ -60,8 +69,27 @@ const isQuantityValid = computed(() => {
|
||||
return Number.isInteger(value) && value > 0;
|
||||
});
|
||||
|
||||
const requiresExtraSaleAudit = computed(() => isExtraSaleAuditProduct(props.orderItem?.product || props.orderItem));
|
||||
|
||||
const isExtraSaleAuditValid = computed(() => {
|
||||
if (!requiresExtraSaleAudit.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!form.extraSaleReasonCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !isExtraSaleCommentRequired(form.extraSaleReasonCode) || form.extraSaleComment.trim().length > 0;
|
||||
});
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
return Boolean(props.orderItem) && props.canEdit && !isSubmitting.value && isPriceValid.value && isQuantityValid.value;
|
||||
return Boolean(props.orderItem)
|
||||
&& props.canEdit
|
||||
&& !isSubmitting.value
|
||||
&& isPriceValid.value
|
||||
&& isQuantityValid.value
|
||||
&& isExtraSaleAuditValid.value;
|
||||
});
|
||||
|
||||
const closeModal = () => {
|
||||
@@ -90,7 +118,11 @@ const saveChanges = async () => {
|
||||
reason_code: props.orderItem.reason_code,
|
||||
reason_label_snapshot: props.orderItem.reason_label_snapshot,
|
||||
reason_comment: form.notes,
|
||||
})
|
||||
}),
|
||||
{
|
||||
extra_sale_reason_code: form.extraSaleReasonCode || null,
|
||||
extra_sale_comment: form.extraSaleComment.trim() || null,
|
||||
}
|
||||
);
|
||||
emits('saved');
|
||||
} catch (error) {
|
||||
@@ -177,6 +209,41 @@ const saveChanges = async () => {
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<template v-if="requiresExtraSaleAudit">
|
||||
<div class="column is-12-mobile is-6-tablet">
|
||||
<label class="label" for="pos-order-item-extra-sale-reason">Årsag</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select
|
||||
id="pos-order-item-extra-sale-reason"
|
||||
v-model="form.extraSaleReasonCode"
|
||||
:disabled="!canEdit"
|
||||
data-testid="pos-order-item-extra-sale-reason"
|
||||
>
|
||||
<option value="">Vælg godkendt årsag</option>
|
||||
<option
|
||||
v-for="reason in EXTRA_SALE_REASONS"
|
||||
:key="reason.code"
|
||||
:value="reason.code"
|
||||
>
|
||||
{{ reason.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="column is-12-mobile is-6-tablet">
|
||||
<label class="label" for="pos-order-item-extra-sale-comment">Kommentar</label>
|
||||
<input
|
||||
id="pos-order-item-extra-sale-comment"
|
||||
v-model="form.extraSaleComment"
|
||||
class="input"
|
||||
type="text"
|
||||
:disabled="!canEdit"
|
||||
data-testid="pos-order-item-extra-sale-comment"
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="column is-12">
|
||||
<label class="label" for="pos-order-item-edit-reference">{{ t('common.reference') }}</label>
|
||||
<input
|
||||
|
||||
@@ -64,6 +64,7 @@ import { pendingBookings } from "@/components/shop/POSDepartmentProcess.vue";
|
||||
import { useOrderMetadataAutosave } from "@/composables/useOrderMetadataAutosave.js";
|
||||
import { useHoldToTrigger } from "@/composables/useHoldToTrigger";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { promptExtraSaleAuditIfRequired } from "@/components/displays/department/pos/utils/extraSaleAudit.js";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import Swal from "sweetalert2";
|
||||
|
||||
export const EXTRA_SALE_PRODUCT_ID = 27;
|
||||
export const EXTRA_SALE_PRODUCT_NAME = "Ekstraordinær pr. 10 min inkl. kemi";
|
||||
|
||||
export const EXTRA_SALE_REASONS = [
|
||||
{ code: "customer_request", label: "Kunde ønskede ekstra tid", commentRequired: false },
|
||||
{ code: "operational_delay", label: "Driftsforsinkelse i vaskehal", commentRequired: false },
|
||||
{ code: "rewash_quality", label: "Omkørsel/kvalitet", commentRequired: false },
|
||||
{ code: "other", label: "Anden godkendt årsag", commentRequired: true },
|
||||
];
|
||||
|
||||
export const isExtraSaleAuditProduct = (product) => {
|
||||
if (!product || typeof product !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (product.requires_extra_sale_audit === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Number(product.id ?? product.product_id ?? 0) === EXTRA_SALE_PRODUCT_ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return String(product.name ?? "").trim() === EXTRA_SALE_PRODUCT_NAME;
|
||||
};
|
||||
|
||||
export const isExtraSaleCommentRequired = (reasonCode) => {
|
||||
return EXTRA_SALE_REASONS.some((reason) => reason.code === reasonCode && reason.commentRequired);
|
||||
};
|
||||
|
||||
export const extraSaleAuditPayload = ({ reasonCode = null, comment = null } = {}) => ({
|
||||
extra_sale_reason_code: String(reasonCode || "").trim() || null,
|
||||
extra_sale_comment: String(comment || "").trim() || null,
|
||||
});
|
||||
|
||||
export const getExtraSaleAuditFromOrderItem = (orderItem = {}) =>
|
||||
extraSaleAuditPayload({
|
||||
reasonCode: orderItem.extra_sale_reason_code,
|
||||
comment: orderItem.extra_sale_comment,
|
||||
});
|
||||
|
||||
export const promptExtraSaleAuditIfRequired = async (product, initial = {}) => {
|
||||
if (!isExtraSaleAuditProduct(product)) {
|
||||
return extraSaleAuditPayload(initial);
|
||||
}
|
||||
|
||||
const initialReason = String(initial.reasonCode ?? initial.extra_sale_reason_code ?? "").trim();
|
||||
const initialComment = String(initial.comment ?? initial.extra_sale_comment ?? "").trim();
|
||||
const optionsMarkup = EXTRA_SALE_REASONS.map((reason) => {
|
||||
const selected = reason.code === initialReason ? " selected" : "";
|
||||
return `<option value="${reason.code}"${selected}>${reason.label}</option>`;
|
||||
}).join("");
|
||||
|
||||
const result = await Swal.fire({
|
||||
title: "Godkend 10 min ekstra",
|
||||
html: `
|
||||
<label class="label has-text-left" for="extra-sale-reason-code">Årsag</label>
|
||||
<div class="select is-fullwidth mb-3">
|
||||
<select id="extra-sale-reason-code" class="swal2-select" style="display:block;width:100%;margin:0;">
|
||||
<option value="">Vælg godkendt årsag</option>
|
||||
${optionsMarkup}
|
||||
</select>
|
||||
</div>
|
||||
<label class="label has-text-left" for="extra-sale-comment">Kommentar</label>
|
||||
<textarea id="extra-sale-comment" class="swal2-textarea" rows="4" style="display:block;width:100%;margin:0;" placeholder="Uddyb når årsagen kræver det">${initialComment}</textarea>
|
||||
`,
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Godkend",
|
||||
focusConfirm: false,
|
||||
preConfirm: () => {
|
||||
const reasonCode = document.getElementById("extra-sale-reason-code")?.value || "";
|
||||
const comment = document.getElementById("extra-sale-comment")?.value || "";
|
||||
|
||||
if (!reasonCode) {
|
||||
Swal.showValidationMessage("Vælg en godkendt årsag");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isExtraSaleCommentRequired(reasonCode) && !String(comment || "").trim()) {
|
||||
Swal.showValidationMessage("Kommentar er påkrævet for denne årsag");
|
||||
return false;
|
||||
}
|
||||
|
||||
return extraSaleAuditPayload({ reasonCode, comment });
|
||||
},
|
||||
});
|
||||
|
||||
return result.isConfirmed ? result.value : null;
|
||||
};
|
||||
@@ -30,6 +30,10 @@ import WhiteBox from "@/components/displays/boxes/WhiteBox.vue";
|
||||
import ProductBox from "@/components/displays/boxes/ProductBox.vue";
|
||||
import { addOrderItemAddons } from "@/components/displays/department/pos/utils/addOrderItemAddons.js";
|
||||
import { getPicture } from "@/components/displays/department/pos/displays/Piktogrammer.vue";
|
||||
import {
|
||||
getExtraSaleAuditFromOrderItem,
|
||||
promptExtraSaleAuditIfRequired,
|
||||
} from "@/components/displays/department/pos/utils/extraSaleAudit.js";
|
||||
|
||||
const emits = defineEmits(['onAddToCartProduct', 'onAddProduct', 'onSelectProduct', 'onSelectionInvalidated']);
|
||||
const route = useRoute();
|
||||
@@ -386,10 +390,15 @@ const warnIfRestrictedAddonsWereSkipped = async (restrictedSelections = []) => {
|
||||
await showRestrictionWarning("pos.restrictions.restricted_items_removed");
|
||||
};
|
||||
|
||||
const resolveExtraSaleAudit = async (product, initial = {}) => {
|
||||
const audit = await promptExtraSaleAuditIfRequired(product, initial);
|
||||
return audit === null ? null : audit;
|
||||
};
|
||||
|
||||
|
||||
|
||||
const showAddMultipleProducts = (productId) => {
|
||||
|
||||
const showAddMultipleProducts = async (productId) => {
|
||||
const product = findProductById(productId);
|
||||
const restriction = getScopedProductRestriction(product);
|
||||
if (restriction.restricted) {
|
||||
@@ -397,7 +406,7 @@ const showAddMultipleProducts = (productId) => {
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
const quantityResult = await Swal.fire({
|
||||
title: 'Tilføj flere produkter',
|
||||
input: 'number',
|
||||
inputAttributes: {
|
||||
@@ -405,7 +414,7 @@ const showAddMultipleProducts = (productId) => {
|
||||
},
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Tilføj',
|
||||
showLoaderOnConfirm: true,
|
||||
showLoaderOnConfirm: false,
|
||||
preConfirm: (inputValue) => {
|
||||
// Check if the input number is higher than 200
|
||||
if (inputValue > 200) {
|
||||
@@ -423,17 +432,32 @@ const showAddMultipleProducts = (productId) => {
|
||||
Swal.showValidationMessage('Order ID is required');
|
||||
return false;
|
||||
}
|
||||
return createOrderItem(orderId, productId, inputValue)
|
||||
.then(async (result) => {
|
||||
const createdItemId = Number(result?.data?.data?.id ?? 0);
|
||||
await addAddonsToOrderMiddleware(productId, inputValue, createdItemId, orderId);
|
||||
await loadOrderItems();
|
||||
})
|
||||
.catch((error) => handleCreateOrderItemError(error, { validationMessage: true }).then(() => false));
|
||||
return Number(inputValue);
|
||||
}
|
||||
},
|
||||
allowOutsideClick: () => !Swal.isLoading()
|
||||
});
|
||||
if (!quantityResult.isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const orderId = getValidOrderId();
|
||||
if (!orderId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const audit = await resolveExtraSaleAudit(product);
|
||||
if (audit === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createOrderItem(orderId, productId, quantityResult.value, null, null, null, audit)
|
||||
.then(async (result) => {
|
||||
const createdItemId = Number(result?.data?.data?.id ?? 0);
|
||||
await addAddonsToOrderMiddleware(productId, quantityResult.value, createdItemId, orderId);
|
||||
await loadOrderItems();
|
||||
})
|
||||
.catch((error) => handleCreateOrderItemError(error));
|
||||
};
|
||||
|
||||
const EXTRAORDINARY_CHEMISTRY_PRODUCT_NAME = "Ekstraordinær pr. 10 min inkl. kemi";
|
||||
@@ -685,7 +709,7 @@ const addProductWithAddonsToOrder = async (product_id) => {
|
||||
// Check if the product requires a note
|
||||
if (productRequiresOrderItemNote(product)) {
|
||||
// Show the note input
|
||||
await Swal.fire({
|
||||
const noteResult = await Swal.fire({
|
||||
title: 'Tilføj en note',
|
||||
input: 'text',
|
||||
inputLabel: 'Noten kan ses af kunden. F.eks. "Fjernelse af graffiti på venstre side"',
|
||||
@@ -694,35 +718,32 @@ const addProductWithAddonsToOrder = async (product_id) => {
|
||||
},
|
||||
showCancelButton: true,
|
||||
confirmButtonText: 'Tilføj',
|
||||
showLoaderOnConfirm: true,
|
||||
showLoaderOnConfirm: false,
|
||||
inputValidator: (note) => {
|
||||
if (!String(note || '').trim()) {
|
||||
return 'Note er påkrævet for dette produkt';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
preConfirm: (note) => {
|
||||
const normalizedNote = String(note || '').trim();
|
||||
const confirmedOrderId = getValidOrderId();
|
||||
if (!confirmedOrderId) {
|
||||
Swal.showValidationMessage('Order ID is required');
|
||||
return false;
|
||||
preConfirm: (note) => String(note || '').trim(),
|
||||
});
|
||||
if (!noteResult.isConfirmed) {
|
||||
return;
|
||||
}
|
||||
const normalizedNote = noteResult.value;
|
||||
const audit = await resolveExtraSaleAudit(product);
|
||||
if (audit === null) {
|
||||
return;
|
||||
}
|
||||
// Show the fake create order item
|
||||
showPendingCreateOrderItem(product, 1, getUserProductPrice(product), 0, normalizedNote);
|
||||
// Create the order item
|
||||
return createOrderItem(confirmedOrderId, product_id, 1, 0, normalizedNote)
|
||||
await createOrderItem(orderId, product_id, 1, 0, normalizedNote, null, audit)
|
||||
.then(async (result) => {
|
||||
let order_item_id = result.data.data.id;
|
||||
// Add the addons to the order
|
||||
await addAddonsToOrderMiddleware(product_id, 1, order_item_id, confirmedOrderId).then(() => {
|
||||
await addAddonsToOrderMiddleware(product_id, 1, order_item_id, orderId).then(() => {
|
||||
loadOrderItems();
|
||||
});
|
||||
})
|
||||
.catch((error) => handleCreateOrderItemError(error, { validationMessage: true }).then(() => false));
|
||||
},
|
||||
allowOutsideClick: () => !Swal.isLoading()
|
||||
});
|
||||
.catch((error) => handleCreateOrderItemError(error));
|
||||
return;
|
||||
}
|
||||
// If the product requires a note, show the note input
|
||||
@@ -739,7 +760,12 @@ const addProductWithAddonsToOrder = async (product_id) => {
|
||||
return;
|
||||
}
|
||||
// Create the order item
|
||||
await createOrderItem(orderId, product_id, quantity)
|
||||
const audit = await resolveExtraSaleAudit(product);
|
||||
if (audit === null) {
|
||||
clearPendingOrderItems();
|
||||
return;
|
||||
}
|
||||
await createOrderItem(orderId, product_id, quantity, null, null, null, audit)
|
||||
.then(async (result) => {
|
||||
let order_item_id = result.data.data.id;
|
||||
// Add the addons to the order
|
||||
@@ -816,12 +842,19 @@ const addPreviousOrderToCurrent = async (previousOrder) => {
|
||||
const discountedPrice = String(getUserProductPrice({ ...previousOrderProduct, price: basePrice }));
|
||||
|
||||
showPendingCreateOrderItem({ ...previousOrderProduct, price: basePrice }, quantity, discountedPrice);
|
||||
const audit = await resolveExtraSaleAudit(previousOrderProduct, getExtraSaleAuditFromOrderItem(orderItem));
|
||||
if (audit === null) {
|
||||
clearPendingOrderItems();
|
||||
continue;
|
||||
}
|
||||
const result = await createOrderItem(
|
||||
orderId,
|
||||
productId,
|
||||
quantity,
|
||||
null,
|
||||
String(orderItem?.notes ?? "")
|
||||
String(orderItem?.notes ?? ""),
|
||||
null,
|
||||
audit
|
||||
).catch((error) => handleCreateOrderItemError(error));
|
||||
const sourceItemId = getPreviousOrderItemId(orderItem);
|
||||
const createdItemId = Number(result?.data?.data?.id ?? 0);
|
||||
@@ -854,12 +887,19 @@ const addPreviousOrderToCurrent = async (previousOrder) => {
|
||||
String(getUserProductPrice(previousOrderProduct)),
|
||||
relatedItemId
|
||||
);
|
||||
const audit = await resolveExtraSaleAudit(previousOrderProduct, getExtraSaleAuditFromOrderItem(orderItem));
|
||||
if (audit === null) {
|
||||
clearPendingOrderItems();
|
||||
continue;
|
||||
}
|
||||
await createOrderItem(
|
||||
orderId,
|
||||
productId,
|
||||
quantity,
|
||||
relatedItemId,
|
||||
String(orderItem?.notes ?? "")
|
||||
String(orderItem?.notes ?? ""),
|
||||
null,
|
||||
audit
|
||||
).catch((error) => handleCreateOrderItemError(error));
|
||||
}
|
||||
|
||||
@@ -923,7 +963,12 @@ const addRecommendedProductToOrder = async (productId) => {
|
||||
}
|
||||
|
||||
showPendingCreateOrderItem(product, 1, getRecommendedProductPrice(productId));
|
||||
const result = await createOrderItem(orderId, productId, 1).catch((error) => handleCreateOrderItemError(error));
|
||||
const audit = await resolveExtraSaleAudit(product);
|
||||
if (audit === null) {
|
||||
clearPendingOrderItems();
|
||||
return;
|
||||
}
|
||||
const result = await createOrderItem(orderId, productId, 1, null, null, null, audit).catch((error) => handleCreateOrderItemError(error));
|
||||
const createdItemId = Number(result?.data?.data?.id ?? 0);
|
||||
if (createdItemId > 0) {
|
||||
await addAddonsToOrderMiddleware(productId, 1, createdItemId, orderId);
|
||||
|
||||
@@ -56,7 +56,16 @@ export const getOrderItems = (order_id) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const createOrderItem = (order_id, product_id, quantity, related_item_id = null, notes = null, forcePrice = null, reasonData = null) => {
|
||||
export const createOrderItem = (
|
||||
order_id,
|
||||
product_id,
|
||||
quantity,
|
||||
related_item_id = null,
|
||||
notes = null,
|
||||
forcePrice = null,
|
||||
reasonData = null,
|
||||
extraSaleAudit = {}
|
||||
) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
return null;
|
||||
@@ -72,6 +81,7 @@ export const createOrderItem = (order_id, product_id, quantity, related_item_id
|
||||
related_item_id,
|
||||
notes,
|
||||
...buildAuditedOrderItemReasonPayload(product_id, notes, reasonData),
|
||||
...extraSaleAudit
|
||||
};
|
||||
if (forcePrice !== null && forcePrice !== undefined) {
|
||||
payload.price = forcePrice;
|
||||
@@ -95,7 +105,7 @@ export const removeOrderItem = (id) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const editOrderItem = (id, price, notes, reference, quantity, reasonData = null) => {
|
||||
export const editOrderItem = (id, price, notes, reference, quantity, reasonData = null, extraSaleAudit = {}) => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
return null;
|
||||
@@ -107,6 +117,7 @@ export const editOrderItem = (id, price, notes, reference, quantity, reasonData
|
||||
reference,
|
||||
quantity,
|
||||
...(reasonData && typeof reasonData === "object" ? reasonData : {}),
|
||||
...extraSaleAudit
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
|
||||
@@ -10,6 +10,10 @@ import { getAttributes } from "@/components/shop/CustomerAttributes.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
|
||||
import { doesOrderContainWashCertificateProduct } from "@/components/displays/department/pos/utils/washCertificate.js";
|
||||
import {
|
||||
getExtraSaleAuditFromOrderItem,
|
||||
promptExtraSaleAuditIfRequired,
|
||||
} from "@/components/displays/department/pos/utils/extraSaleAudit.js";
|
||||
import {
|
||||
getCustomerProductRestriction,
|
||||
getProductCategoryRestrictionForCustomer,
|
||||
@@ -1572,12 +1576,19 @@ const copyLastWashReferenceToEmptyCurrentOrder = async (sourceReference) => {
|
||||
}
|
||||
};
|
||||
|
||||
const createCopiedOrderItem = (targetOrderId, sourceItem, relatedItemId = null) => {
|
||||
const createCopiedOrderItem = async (targetOrderId, sourceItem, relatedItemId = null) => {
|
||||
const productId = getOrderItemProductId(sourceItem);
|
||||
const quantity = getOrderItemQuantity(sourceItem);
|
||||
if (!productId || !quantity) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
const audit = await promptExtraSaleAuditIfRequired(
|
||||
sourceItem?.product || { id: productId },
|
||||
getExtraSaleAuditFromOrderItem(sourceItem)
|
||||
);
|
||||
if (audit === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Preserve audited-order-item reason metadata from the source row so the
|
||||
// copied POST satisfies the server-side `reason_comment` requirement for
|
||||
@@ -1600,7 +1611,8 @@ const createCopiedOrderItem = (targetOrderId, sourceItem, relatedItemId = null)
|
||||
relatedItemId,
|
||||
getOrderItemNotes(sourceItem),
|
||||
null,
|
||||
sourceReasonData
|
||||
sourceReasonData,
|
||||
audit
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2627,13 +2639,19 @@ export const hydrateSelectedOrderBookingForDesktop = async () => {
|
||||
}
|
||||
|
||||
const forcedPrimaryPrice = primaryProduct.price ?? null;
|
||||
const primaryAudit = await promptExtraSaleAuditIfRequired(primaryProduct);
|
||||
if (primaryAudit === null) {
|
||||
return false;
|
||||
}
|
||||
const primaryItemResponse = await createOrderItem(
|
||||
normalizedOrderId,
|
||||
primaryProduct.id,
|
||||
1,
|
||||
null,
|
||||
null,
|
||||
forcedPrimaryPrice
|
||||
forcedPrimaryPrice,
|
||||
null,
|
||||
primaryAudit
|
||||
);
|
||||
const relatedPrimaryItemId = toPositiveInteger(primaryItemResponse?.data?.data?.id);
|
||||
|
||||
@@ -2645,6 +2663,10 @@ export const hydrateSelectedOrderBookingForDesktop = async () => {
|
||||
}
|
||||
|
||||
const secondaryProduct = await fetchOrderBookingProductWithPricing(secondaryProductId);
|
||||
const secondaryAudit = await promptExtraSaleAuditIfRequired(secondaryProduct || { id: secondaryProductId });
|
||||
if (secondaryAudit === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await createOrderItem(
|
||||
normalizedOrderId,
|
||||
@@ -2652,7 +2674,9 @@ export const hydrateSelectedOrderBookingForDesktop = async () => {
|
||||
Math.max(1, Number.parseInt(String(bookingItem?.quantity ?? 1), 10) || 1),
|
||||
relatedPrimaryItemId,
|
||||
String(bookingItem?.notes ?? "").trim() || null,
|
||||
secondaryProduct?.price ?? null
|
||||
secondaryProduct?.price ?? null,
|
||||
null,
|
||||
secondaryAudit
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -777,8 +777,14 @@ describe("POSDepartmentProcess.hydrateSelectedOrderBookingForDesktop", () => {
|
||||
|
||||
await expect(hydrateSelectedOrderBookingForDesktop()).resolves.toBe(true);
|
||||
|
||||
expect(mocks.createOrderItem).toHaveBeenNthCalledWith(1, 51207, 10, 1, null, null, 500);
|
||||
expect(mocks.createOrderItem).toHaveBeenNthCalledWith(2, 51207, 20, 3, 9001, "Addon note", 500);
|
||||
expect(mocks.createOrderItem).toHaveBeenNthCalledWith(1, 51207, 10, 1, null, null, 500, null, {
|
||||
extra_sale_comment: null,
|
||||
extra_sale_reason_code: null,
|
||||
});
|
||||
expect(mocks.createOrderItem).toHaveBeenNthCalledWith(2, 51207, 20, 3, 9001, "Addon note", 500, null, {
|
||||
extra_sale_comment: null,
|
||||
extra_sale_reason_code: null,
|
||||
});
|
||||
expect(SessionUser.objects.products.get.single).toHaveBeenCalledWith(10, {
|
||||
department_id: 2,
|
||||
customer_id: 12345679,
|
||||
|
||||
Reference in New Issue
Block a user