Add tests and utilities for dynamic booking preload handling in POS flow:
- Introduced logic and utilities to fetch exact plate bookings beyond initial preload (`ensureVehiclePlateBookingsLoaded`, `mergePendingBookings`). - Enhanced booking dropdown and chooser to handle dynamically loaded data for desktop POS. - Updated e2e tests (`pos-flow.spec.js`) with scenarios for exact and partial plate matches in booking preload handling. - Refactored `network.js` to support paginated and filtered responses for bookings based on registration fields. - Improved UX and display logic in booking selectors (`LicensePlateReg1Input.vue`, `PosDepartmentProcess.vue`).
This commit is contained in:
@@ -25,6 +25,10 @@ import DefaultObjectSelector from "@/components/displays/modals/DefaultObjectSel
|
||||
import PosDesktopOrderBookingSelectorModal from "@/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { parsePosRouteSearch } from "@/views/dashboards/departmentDashboard/modules/Pos/posRouteState.js";
|
||||
import {
|
||||
getOrderBookingReferenceValue,
|
||||
getOrderBookingServiceText,
|
||||
} from "@/components/displays/department/pos/utils/orderBookingDisplay.js";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
@@ -179,18 +183,6 @@ const formatBookingDateTime = (booking) => {
|
||||
}).format(parsedValue);
|
||||
};
|
||||
|
||||
const getBookingReferenceValue = (booking) => {
|
||||
return String(booking?.reference ?? booking?.reference_number ?? "").trim();
|
||||
};
|
||||
|
||||
const getBookingServicesValue = (booking) => {
|
||||
if (Array.isArray(booking?.parsed_services?.array) && booking.parsed_services.array.length > 0) {
|
||||
return booking.parsed_services.array.join(", ");
|
||||
}
|
||||
|
||||
return String(booking?.parsed_services?.string ?? booking?.wash_type ?? "").trim();
|
||||
};
|
||||
|
||||
const formatDuplicateOrderDate = (value) => {
|
||||
if (!value) {
|
||||
return t("admin.pos.not_found");
|
||||
@@ -221,8 +213,8 @@ const bookingSelectionObjects = computed(() => {
|
||||
const contentSegments = [
|
||||
`${t("admin.pos.order_booking_selector.customer_label")}: ${booking?.customer_name || t("admin.pos.not_found")}`,
|
||||
`${t("admin.pos.order_booking_selector.plates_label")}: ${plateText || t("admin.pos.not_found")}`,
|
||||
`${t("admin.pos.order_booking_selector.reference_label")}: ${getBookingReferenceValue(booking) || t("admin.pos.not_found")}`,
|
||||
`${t("admin.pos.order_booking_selector.services_label")}: ${getBookingServicesValue(booking) || t("admin.pos.not_found")}`,
|
||||
`${t("admin.pos.order_booking_selector.reference_label")}: ${getOrderBookingReferenceValue(booking) || t("admin.pos.not_found")}`,
|
||||
`${t("admin.pos.order_booking_selector.services_label")}: ${getOrderBookingServiceText(booking) || t("admin.pos.not_found")}`,
|
||||
];
|
||||
|
||||
return {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
pendingBookings,
|
||||
hasLoadedPendingBookings,
|
||||
loadPendingBookings,
|
||||
ensureVehiclePlateBookingsLoaded,
|
||||
doesVehiclePlateHaveBooking,
|
||||
getVehiclePlateBookings,
|
||||
getPreferredVehiclePlateBooking,
|
||||
@@ -144,16 +145,39 @@ const getBookingMatchesForSelection = (vehicle = null, plateOverride = null) =>
|
||||
};
|
||||
|
||||
const ensureBookingMatchesForSelection = async (vehicle = null, plateOverride = null) => {
|
||||
let bookingMatches = getBookingMatchesForSelection(vehicle, plateOverride);
|
||||
const bookingPlate = resolveBookingPlate(vehicle, plateOverride);
|
||||
let bookingMatches = getBookingMatchesForSelection(vehicle, bookingPlate);
|
||||
|
||||
if (!hasLoadedPendingBookings.value || (bookingMatches.length === 0 && vehicleIndicatesBooking(vehicle))) {
|
||||
await loadPendingBookings();
|
||||
bookingMatches = getBookingMatchesForSelection(vehicle, plateOverride);
|
||||
bookingMatches = getBookingMatchesForSelection(vehicle, bookingPlate);
|
||||
}
|
||||
|
||||
if (bookingPlate && vehicleIndicatesBooking(vehicle)) {
|
||||
await ensureVehiclePlateBookingsLoaded(bookingPlate, {
|
||||
force: bookingMatches.length <= 1,
|
||||
});
|
||||
bookingMatches = getBookingMatchesForSelection(vehicle, bookingPlate);
|
||||
}
|
||||
|
||||
return bookingMatches;
|
||||
};
|
||||
|
||||
const prefetchVisibleBookedVehicleBookings = (vehicles = []) => {
|
||||
vehicles
|
||||
.filter((vehicle) => vehicleIndicatesBooking(vehicle))
|
||||
.forEach((vehicle) => {
|
||||
const bookingPlate = resolveBookingPlate(vehicle, vehicle?.reg);
|
||||
if (!bookingPlate) {
|
||||
return;
|
||||
}
|
||||
|
||||
void ensureVehiclePlateBookingsLoaded(bookingPlate, {
|
||||
force: getBookingMatchesForSelection(vehicle, bookingPlate).length <= 1,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Function to emit the booking object to the parent component
|
||||
const emitBookingObject = (vehicle, plateOverride = null) => {
|
||||
const bookingMatches = getBookingMatchesForSelection(vehicle, plateOverride);
|
||||
@@ -636,6 +660,7 @@ const isSearching = ref(false);
|
||||
|
||||
// Watch for changes in the vehicles_matching array, to check if the current index is valid
|
||||
watch(vehicles_matching, (newValue) => {
|
||||
prefetchVisibleBookedVehicleBookings(newValue);
|
||||
// Check if the input directly matches an item in the vehicles_matching array (Then we can automatically select it)
|
||||
const currentValue = reg_1.value;
|
||||
const vehicle = newValue.find((vehicle) => vehicle.reg === currentValue);
|
||||
|
||||
@@ -1398,6 +1398,9 @@ export const pendingBookings = ref([]);
|
||||
export const hasLoadedPendingBookings = ref(false);
|
||||
export const isLoadingPendingBookings = ref(false);
|
||||
let pendingBookingsRequest = null;
|
||||
const pendingPlateBookingRequests = new Map();
|
||||
const loadedPendingBookingPlates = new Set();
|
||||
let pendingBookingsDepartmentKey = null;
|
||||
|
||||
export const is_latest_search = (search_id) => {
|
||||
// Check if the search ID is the latest
|
||||
@@ -1406,6 +1409,15 @@ export const is_latest_search = (search_id) => {
|
||||
|
||||
// Get the department booking list
|
||||
export const loadPendingBookings = () => {
|
||||
const currentDepartmentKey = String(department_id.value ?? "");
|
||||
if (pendingBookingsDepartmentKey !== currentDepartmentKey) {
|
||||
pendingBookingsDepartmentKey = currentDepartmentKey;
|
||||
pendingBookings.value = [];
|
||||
hasLoadedPendingBookings.value = false;
|
||||
pendingPlateBookingRequests.clear();
|
||||
loadedPendingBookingPlates.clear();
|
||||
}
|
||||
|
||||
if (pendingBookingsRequest) {
|
||||
return pendingBookingsRequest;
|
||||
}
|
||||
@@ -1513,6 +1525,12 @@ const dedupeOrderBookings = (bookings = []) => {
|
||||
return Array.from(bookingsByKey.values());
|
||||
};
|
||||
|
||||
const mergePendingBookings = (bookings = []) => {
|
||||
const mergedBookings = dedupeOrderBookings([...(pendingBookings.value || []), ...(bookings || [])]);
|
||||
pendingBookings.value = sortPendingOrderBookings(mergedBookings);
|
||||
return pendingBookings.value;
|
||||
};
|
||||
|
||||
const resolveVehiclePlateBookingMatches = (vehiclePlate, bookingMatches = null) => {
|
||||
if (Array.isArray(bookingMatches)) {
|
||||
return sortPendingOrderBookings(dedupeOrderBookings(bookingMatches));
|
||||
@@ -1553,6 +1571,59 @@ export const getVehiclePlateBookings = (vehiclePlate) => {
|
||||
return sortPendingOrderBookings(matchingBookings);
|
||||
};
|
||||
|
||||
export const ensureVehiclePlateBookingsLoaded = async (vehiclePlate, options = {}) => {
|
||||
const normalizedOptions = {
|
||||
force: false,
|
||||
...options,
|
||||
};
|
||||
const normalizedVehiclePlate = normalizeVehiclePlateForBookingSelection(vehiclePlate);
|
||||
if (!normalizedVehiclePlate || !department_id.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!normalizedOptions.force && loadedPendingBookingPlates.has(normalizedVehiclePlate)) {
|
||||
return getVehiclePlateBookings(normalizedVehiclePlate);
|
||||
}
|
||||
|
||||
if (pendingPlateBookingRequests.has(normalizedVehiclePlate)) {
|
||||
return pendingPlateBookingRequests.get(normalizedVehiclePlate);
|
||||
}
|
||||
|
||||
const fetchBookingsForField = async (field) => {
|
||||
const filters = [`department:${department_id.value}`, `${field}:${normalizedVehiclePlate}`, "order_id:is null"].join(
|
||||
","
|
||||
);
|
||||
const response = await SessionUser.request(SessionUser.objects.order_bookings.meta.endpoint, "GET", {
|
||||
filters,
|
||||
page: 1,
|
||||
limit: 250,
|
||||
});
|
||||
return Array.isArray(response?.data?.data) ? response.data.data : [];
|
||||
};
|
||||
|
||||
const requestPromise = Promise.all([
|
||||
fetchBookingsForField("reg_1"),
|
||||
fetchBookingsForField("reg_2"),
|
||||
])
|
||||
.then(([reg1Bookings, reg2Bookings]) => {
|
||||
const fetchedBookings = sortPendingOrderBookings(dedupeOrderBookings([...reg1Bookings, ...reg2Bookings]));
|
||||
mergePendingBookings(fetchedBookings);
|
||||
loadedPendingBookingPlates.add(normalizedVehiclePlate);
|
||||
return getVehiclePlateBookings(normalizedVehiclePlate);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Unable to load pending bookings for plate:", normalizedVehiclePlate, error);
|
||||
loadedPendingBookingPlates.add(normalizedVehiclePlate);
|
||||
return getVehiclePlateBookings(normalizedVehiclePlate);
|
||||
})
|
||||
.finally(() => {
|
||||
pendingPlateBookingRequests.delete(normalizedVehiclePlate);
|
||||
});
|
||||
|
||||
pendingPlateBookingRequests.set(normalizedVehiclePlate, requestPromise);
|
||||
return requestPromise;
|
||||
};
|
||||
|
||||
export const doesVehiclePlateHaveBooking = (vehiclePlate) => {
|
||||
return getVehiclePlateBookings(vehiclePlate).length > 0;
|
||||
};
|
||||
|
||||
@@ -1098,6 +1098,126 @@ test.describe("POS flow", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("desktop hydrates booked dropdown dates and chooser from exact plate bookings beyond the initial preload", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name !== "chromium-desktop",
|
||||
"Desktop POS booking flow is validated on chromium-desktop."
|
||||
);
|
||||
|
||||
const fixture = createPosFixture();
|
||||
fixture.vehicles = [
|
||||
{
|
||||
...fixture.vehicles[0],
|
||||
id: 7101,
|
||||
reg: "EC21233",
|
||||
booking_id: 9101,
|
||||
status: "booked",
|
||||
},
|
||||
{
|
||||
...fixture.vehicles[0],
|
||||
id: 7102,
|
||||
reg: "EC21234",
|
||||
booking_id: 9102,
|
||||
status: "booked",
|
||||
},
|
||||
];
|
||||
fixture.orderBookings = [
|
||||
...Array.from({ length: 100 }, (_, index) =>
|
||||
buildOrderBooking(9200 + index, {
|
||||
reg_1: `FILL${String(index + 1).padStart(4, "0")}`,
|
||||
datetime: `2026-01-${String((index % 28) + 1).padStart(2, "0")}T06:00:00.000Z`,
|
||||
})
|
||||
),
|
||||
buildOrderBooking(9102, {
|
||||
reg_1: "EC21234",
|
||||
datetime: "2026-03-20T08:00:00.000Z",
|
||||
reference: "PRELOAD-MISS-A",
|
||||
reference_number: "PRELOAD-MISS-A",
|
||||
}),
|
||||
buildOrderBooking(9103, {
|
||||
reg_1: "EC21234",
|
||||
datetime: "2026-03-21T09:00:00.000Z",
|
||||
reference: "PRELOAD-MISS-B",
|
||||
reference_number: "PRELOAD-MISS-B",
|
||||
}),
|
||||
];
|
||||
|
||||
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booking-preload-miss" });
|
||||
|
||||
const activeBookingSelector = getActiveDesktopModal(page);
|
||||
await page.locator("#reg_1").fill("EC212");
|
||||
await expect(page.getByTestId("desktop-booked-date-7102")).toContainText("20.03.2026", { timeout: 10_000 });
|
||||
|
||||
await page
|
||||
.locator(".dropdown-item")
|
||||
.filter({ hasText: "EC21234 - Pleno Logistics" })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
|
||||
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-9102")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-9103")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("desktop exact reg entry opens the booking chooser when exact plate bookings were not in the initial preload", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name !== "chromium-desktop",
|
||||
"Desktop POS booking flow is validated on chromium-desktop."
|
||||
);
|
||||
|
||||
const fixture = createPosFixture();
|
||||
fixture.vehicles = [
|
||||
{
|
||||
...fixture.vehicles[0],
|
||||
id: 7110,
|
||||
reg: "EC21234",
|
||||
booking_id: 9110,
|
||||
status: "booked",
|
||||
},
|
||||
];
|
||||
fixture.orderBookings = [
|
||||
...Array.from({ length: 100 }, (_, index) =>
|
||||
buildOrderBooking(9300 + index, {
|
||||
reg_1: `MISS${String(index + 1).padStart(4, "0")}`,
|
||||
datetime: `2026-02-${String((index % 28) + 1).padStart(2, "0")}T06:00:00.000Z`,
|
||||
})
|
||||
),
|
||||
buildOrderBooking(9110, {
|
||||
reg_1: "EC21234",
|
||||
datetime: "2026-03-20T08:00:00.000Z",
|
||||
reference: "EXACT-PRELOAD-MISS-A",
|
||||
reference_number: "EXACT-PRELOAD-MISS-A",
|
||||
}),
|
||||
buildOrderBooking(9111, {
|
||||
reg_1: "EC21234",
|
||||
datetime: "2026-03-21T09:00:00.000Z",
|
||||
reference: "EXACT-PRELOAD-MISS-B",
|
||||
reference_number: "EXACT-PRELOAD-MISS-B",
|
||||
}),
|
||||
];
|
||||
|
||||
await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booking-preload-miss-exact" });
|
||||
|
||||
const activeBookingSelector = getActiveDesktopModal(page);
|
||||
await commitDesktopReg1ByBlur(page, "EC21234");
|
||||
|
||||
await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
|
||||
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-9110")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await expect(activeBookingSelector.getByTestId("pos-desktop-order-booking-use-9111")).toBeVisible({
|
||||
timeout: 10_000,
|
||||
});
|
||||
});
|
||||
|
||||
test("desktop auto-applies a single matching order booking and completes that booking", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
|
||||
@@ -1245,6 +1245,8 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
|
||||
if (pathname.endsWith("/order-bookings") && method === "GET") {
|
||||
const id = Number(parsedUrl.searchParams.get("id") || 0);
|
||||
const filters = String(parsedUrl.searchParams.get("filters") || "");
|
||||
const page = Number(parsedUrl.searchParams.get("page") || 1);
|
||||
const limit = Number(parsedUrl.searchParams.get("limit") || 100);
|
||||
let bookings = Array.isArray(posFixture.orderBookings) ? posFixture.orderBookings : [];
|
||||
|
||||
if (id > 0) {
|
||||
@@ -1268,6 +1270,16 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
|
||||
bookings = bookings.filter((booking) => String(booking.reg_1 || "").toUpperCase() === String(reg).toUpperCase());
|
||||
}
|
||||
|
||||
if (filters.includes("reg_2:")) {
|
||||
const reg = filters.split("reg_2:")[1]?.split(",")[0] || "";
|
||||
bookings = bookings.filter((booking) => String(booking.reg_2 || "").toUpperCase() === String(reg).toUpperCase());
|
||||
}
|
||||
|
||||
const normalizedPage = Number.isFinite(page) && page > 0 ? page : 1;
|
||||
const normalizedLimit = Number.isFinite(limit) && limit > 0 ? limit : bookings.length || 1;
|
||||
const offset = (normalizedPage - 1) * normalizedLimit;
|
||||
bookings = bookings.slice(offset, offset + normalizedLimit);
|
||||
|
||||
await route.fulfill(
|
||||
json({
|
||||
success: true,
|
||||
|
||||
Reference in New Issue
Block a user