diff --git a/src/components/forms/department/pos/SelectVehicleFormPOS.vue b/src/components/forms/department/pos/SelectVehicleFormPOS.vue index 06fcbe39..57467668 100644 --- a/src/components/forms/department/pos/SelectVehicleFormPOS.vue +++ b/src/components/forms/department/pos/SelectVehicleFormPOS.vue @@ -11,6 +11,7 @@ import { reg_3, order_notes, order_po, + order_id, searchAndSelectCustomer, selectCustomer, isCustomerSelected, @@ -95,6 +96,10 @@ const getBookingNotesValue = (booking) => { return String(booking?.notes ?? booking?.note ?? '').trim(); }; +const getBookingReg2Value = (booking) => { + return normalizePlateValue(booking?.reg_2 ?? booking?.regNrTrailer ?? ''); +}; + const getBookingCustomerNumber = (booking) => { const parsedValue = Number.parseInt(String(booking?.customer_number ?? booking?.customer_id ?? ''), 10); return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null; @@ -167,6 +172,14 @@ const applySelectedOrderBooking = async (booking) => { resetReferenceFromVehicle(); } + const bookingReg2 = getBookingReg2Value(booking); + if (bookingReg2 !== '') { + reg_2.value = bookingReg2; + if (order_id.value) { + await SessionUser.objects.orders.set.reg_2(order_id.value, bookingReg2); + } + } + order_notes.value = getBookingNotesValue(booking); order_po.value = String(booking?.po ?? '').trim(); diff --git a/src/components/forms/department/pos/input/LicensePlateReg1Input.vue b/src/components/forms/department/pos/input/LicensePlateReg1Input.vue index 174b95e0..30461670 100644 --- a/src/components/forms/department/pos/input/LicensePlateReg1Input.vue +++ b/src/components/forms/department/pos/input/LicensePlateReg1Input.vue @@ -1,6 +1,7 @@ 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(