+
@@ -554,5 +641,35 @@ const getCurrentIconColor = () => {
diff --git a/src/components/shop/POSDepartmentProcess.vue b/src/components/shop/POSDepartmentProcess.vue
index 6944e79d..ea5f1b1d 100644
--- a/src/components/shop/POSDepartmentProcess.vue
+++ b/src/components/shop/POSDepartmentProcess.vue
@@ -1407,6 +1407,10 @@ const getOrderBookingNotesValue = (booking) => {
return String(booking?.notes ?? booking?.note ?? "").trim();
};
+const getOrderBookingReg2Value = (booking) => {
+ return normalizeRegistrationValue(booking?.reg_2 ?? booking?.regNrTrailer ?? "");
+};
+
const getOrderBookingCustomerNumber = (booking) => {
return resolveCustomerNumber(booking?.customer_number ?? booking?.customer_id ?? booking?.customerNumber);
};
@@ -1533,6 +1537,12 @@ const hydrateSelectedOrderBookingForDesktop = async () => {
reference.value = bookingReference;
}
+ const bookingReg2 = getOrderBookingReg2Value(booking);
+ if (bookingReg2 !== "" && bookingReg2 !== reg_2.value) {
+ await SessionUser.objects.orders.set.reg_2(normalizedOrderId, bookingReg2);
+ reg_2.value = bookingReg2;
+ }
+
const bookingNotes = getOrderBookingNotesValue(booking);
if (bookingNotes !== "" && bookingNotes !== order_notes.value) {
await SessionUser.objects.orders.set.notes(normalizedOrderId, bookingNotes);
diff --git a/tests/e2e/pos-flow.spec.js b/tests/e2e/pos-flow.spec.js
index 5206d4e8..579c101e 100644
--- a/tests/e2e/pos-flow.spec.js
+++ b/tests/e2e/pos-flow.spec.js
@@ -331,6 +331,18 @@ async function mockPosApi(page, fixture) {
return;
}
+ if (pathname.endsWith("/vehicles/search") && method === "GET") {
+ const search = (parsedUrl.searchParams.get("search") || "").toUpperCase();
+ const matches = fixture.vehicles.filter((vehicle) => vehicle.reg.includes(search));
+ await route.fulfill(
+ json({
+ success: true,
+ data: matches,
+ })
+ );
+ return;
+ }
+
if (pathname.endsWith("/vehicles") && method === "GET") {
const search = (parsedUrl.searchParams.get("search") || "").toUpperCase();
const matches = fixture.vehicles.filter((vehicle) => vehicle.reg.includes(search));
@@ -805,6 +817,22 @@ test.describe("POS flow", () => {
expect(consoleProblems.join("\n")).not.toContain("reg_1");
});
+ test("desktop hides the Sidste vask section when the selected vehicle has no last_order_id", async ({
+ page,
+ }, testInfo) => {
+ test.skip(testInfo.project.name !== "chromium-desktop", "Desktop POS step 1 is validated on chromium-desktop.");
+
+ const fixture = createPosFixture();
+
+ await setupDesktopPosPage(page, fixture, { token: "pos-no-last-wash-token" });
+
+ await page.locator("#reg_1").fill("AB12345");
+ await expect(page.getByTestId("pos-step-1").getByText("Pleno Logistics").first()).toBeVisible({ timeout: 10_000 });
+
+ await expect(page.getByText(/Sidste vask/i)).toHaveCount(0);
+ await expect(page.getByRole("button", { name: /Kopier sidste vask/i })).toHaveCount(0);
+ });
+
test("desktop flow creates transaction, renders cart, and reaches completion step", async ({ page }, testInfo) => {
test.skip(testInfo.project.name !== "chromium-desktop", "Desktop POS flow is validated on chromium-desktop.");
@@ -889,6 +917,50 @@ test.describe("POS flow", () => {
expect(consoleProblems.join("\n")).not.toContain("Extraneous non-props attributes");
});
+ test("desktop marks booked vehicles in the dropdown when the vehicle payload already carries a booking", 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],
+ reg: "EC21235",
+ booking_id: 8991,
+ status: "booked",
+ },
+ {
+ ...fixture.vehicles[0],
+ id: 7002,
+ reg: "EC21234",
+ booking_id: 8992,
+ status: "booked",
+ },
+ ];
+ fixture.orderBookings = [
+ buildOrderBooking(8991, {
+ reg_1: "EC21235",
+ datetime: "2026-01-03T07:00:00.000Z",
+ }),
+ buildOrderBooking(8992, {
+ reg_1: "EC21234",
+ datetime: "2026-01-04T09:00:00.000Z",
+ }),
+ ];
+
+ await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booked-dropdown" });
+
+ await page.locator("#reg_1").fill("EC212");
+ await expect(page.getByTestId("desktop-booked-icon-7001")).toBeVisible({ timeout: 10_000 });
+ await expect(page.getByTestId("desktop-booked-icon-7002")).toBeVisible({ timeout: 10_000 });
+ await expect(page.getByTestId("desktop-booked-date-7001")).toContainText(/\d/, { timeout: 10_000 });
+ await expect(page.getByTestId("desktop-booked-date-7002")).toContainText(/\d/, { timeout: 10_000 });
+ });
+
test("desktop auto-applies a single matching order booking and completes that booking", async ({
page,
}, testInfo) => {
@@ -902,6 +974,7 @@ test.describe("POS flow", () => {
buildOrderBooking(8101, {
reference: "SINGLE-BOOKING-REF",
reference_number: "SINGLE-BOOKING-REF",
+ reg_2: "TRAILER-1",
notes: "Single desktop booking",
note: "Single desktop booking",
items: [
@@ -920,6 +993,7 @@ test.describe("POS flow", () => {
const activeBookingSelector = page.locator('[data-testid="default-object-selector"].is-active');
await page.locator("#reg_1").fill("AB12345");
await expect(activeBookingSelector).toHaveCount(0);
+ await expect(page.locator("#reg_2")).toHaveValue("TRAILER-1");
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
@@ -927,6 +1001,7 @@ test.describe("POS flow", () => {
await expect
.poll(() => fixture.ordersById[9300]?.reference || null, { timeout: 10_000 })
.toBe("SINGLE-BOOKING-REF");
+ await expect.poll(() => fixture.ordersById[9300]?.reg_2 || null, { timeout: 10_000 }).toBe("TRAILER-1");
await expect
.poll(() => (fixture.orderItemsByOrderId[9300] || []).map((item) => Number(item.product_id)), { timeout: 10_000 })
.toEqual([53, 63]);
@@ -960,6 +1035,7 @@ test.describe("POS flow", () => {
datetime: "2026-01-01T07:00:00.000Z",
reference: "BOOKING-A-REF",
reference_number: "BOOKING-A-REF",
+ reg_2: "TRAILER-A",
items: [
{ id: 53, name: "Tankvogn med hænger", price: 599, quantity: 1 },
{ id: 63, name: "Dolly", price: 275, quantity: 1 },
@@ -973,6 +1049,7 @@ test.describe("POS flow", () => {
datetime: "2026-01-01T09:00:00.000Z",
reference: "BOOKING-B-REF",
reference_number: "BOOKING-B-REF",
+ reg_2: "TRAILER-B",
items: [{ id: 63, name: "Dolly", price: 275, quantity: 1 }],
parsed_services: {
string: "Dolly",
@@ -995,10 +1072,12 @@ test.describe("POS flow", () => {
await activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8102").click();
await expect(page.locator("#reference")).toHaveValue("BOOKING-A-REF");
+ await expect(page.locator("#reg_2")).toHaveValue("TRAILER-A");
await page.locator('[data-testid="pos-next-step"]:visible').click();
await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
+ await expect.poll(() => fixture.ordersById[9300]?.reg_2 || null, { timeout: 10_000 }).toBe("TRAILER-A");
await expect
.poll(() => (fixture.orderItemsByOrderId[9300] || []).map((item) => Number(item.product_id)), { timeout: 10_000 })
.toEqual([53, 63]);
@@ -1018,6 +1097,75 @@ test.describe("POS flow", () => {
expect(fixture.completedBookingIds).not.toContain(8103);
});
+ test("desktop opens the booking chooser for a booked plate even without a vehicle match", async ({
+ page,
+ }, testInfo) => {
+ test.skip(
+ testInfo.project.name !== "chromium-desktop",
+ "Desktop POS booking flow is validated on chromium-desktop."
+ );
+
+ const fixture = createPosFixture();
+ fixture.orderBookings = [
+ buildOrderBooking(8111, {
+ reg_1: "BOOKONLY1",
+ datetime: "2026-01-01T07:00:00.000Z",
+ reference: "BOOK-ONLY-A",
+ reference_number: "BOOK-ONLY-A",
+ items: [
+ { id: 53, name: "Tankvogn med hænger", price: 599, quantity: 1 },
+ { id: 63, name: "Dolly", price: 275, quantity: 1 },
+ ],
+ parsed_services: {
+ string: "Tankvogn med hænger, Dolly",
+ array: ["Tankvogn med hænger", "Dolly"],
+ },
+ }),
+ buildOrderBooking(8112, {
+ reg_1: "BOOKONLY1",
+ datetime: "2026-01-01T09:00:00.000Z",
+ reference: "BOOK-ONLY-B",
+ reference_number: "BOOK-ONLY-B",
+ items: [{ id: 63, name: "Dolly", price: 275, quantity: 1 }],
+ parsed_services: {
+ string: "Dolly",
+ array: ["Dolly"],
+ },
+ }),
+ ];
+
+ await setupDesktopPosPage(page, fixture, { token: "pos-desktop-booking-only" });
+
+ const activeBookingSelector = page.locator('[data-testid="default-object-selector"].is-active');
+ await page.locator("#reg_1").fill("BOOKONLY1");
+ await expect(activeBookingSelector).toBeVisible({ timeout: 10_000 });
+ await activeBookingSelector.getByTestId("pos-desktop-order-booking-use-8111").click();
+ await expect(page.locator("#reference")).toHaveValue("BOOK-ONLY-A");
+
+ await page.locator('[data-testid="pos-next-step"]:visible').click();
+ await expect(page.getByTestId("pos-step-2")).toBeVisible({ timeout: 10_000 });
+ await expect
+ .poll(() => (fixture.orderItemsByOrderId[9300] || []).map((item) => Number(item.product_id)), { timeout: 10_000 })
+ .toEqual([53, 63]);
+
+ await page.locator('[data-testid="pos-next-step"]:visible').click();
+ await expect(page.getByTestId("pos-step-3")).toBeVisible({ timeout: 10_000 });
+ await page.locator('[data-testid="pos-next-step"]:visible').click();
+
+ await expect.poll(() => fixture.ordersById[9300]?.booking_id || null, { timeout: 10_000 }).toBe(8111);
+ await expect
+ .poll(
+ () =>
+ fixture.bookingOrderAssignments.some(
+ (entry) => Number(entry?.id) === 8111 && Number(entry?.order_id) === 9300
+ ),
+ { timeout: 10_000 }
+ )
+ .toBe(true);
+ await expect.poll(() => fixture.completedBookingIds, { timeout: 10_000 }).toContain(8111);
+ expect(fixture.completedBookingIds).not.toContain(8112);
+ });
+
test("desktop continue without booking skips hydration and completion requests", async ({ page }, testInfo) => {
test.skip(
testInfo.project.name !== "chromium-desktop",
diff --git a/tests/e2e/support/network.js b/tests/e2e/support/network.js
index 56f53039..5365621e 100644
--- a/tests/e2e/support/network.js
+++ b/tests/e2e/support/network.js
@@ -1176,7 +1176,9 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
}
if (pathname.endsWith("/bookings") && method === "GET") {
- await route.fulfill(json({ success: true, data: Array.isArray(posFixture.orderBookings) ? posFixture.orderBookings : [] }));
+ await route.fulfill(
+ json({ success: true, data: Array.isArray(posFixture.orderBookings) ? posFixture.orderBookings : [] })
+ );
return true;
}
@@ -1189,9 +1191,7 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
const bookingDate = String(body.datetime || createdAt).slice(0, 10);
const customer = posFixture.customersByNumber?.[customerNumber] || null;
const serviceNames = Array.isArray(body.items)
- ? body.items
- .map((item) => String(item?.name || "").trim())
- .filter(Boolean)
+ ? body.items.map((item) => String(item?.name || "").trim()).filter(Boolean)
: [];
const createdBooking = {
@@ -1334,6 +1334,19 @@ async function handlePosRoute({ route, request, parsedUrl, pathname, method, pos
return true;
}
+ if (pathname.endsWith("/vehicles/search") && method === "GET") {
+ const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
+ const vehicles = (posFixture.vehicles || []).filter(
+ (vehicle) =>
+ !search ||
+ String(vehicle.reg || "")
+ .toUpperCase()
+ .includes(search)
+ );
+ await route.fulfill(json({ success: true, data: vehicles }));
+ return true;
+ }
+
if (pathname.endsWith("/vehicles") && method === "GET") {
const search = String(parsedUrl.searchParams.get("search") || "").toUpperCase();
const vehicles = (posFixture.vehicles || []).filter(