From da65876a641b1069b2001d5d996d79f5e8cc1960 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 6 May 2026 14:14:34 +0200 Subject: [PATCH] Add responsive tests for "/qr/new-customer" layout, extend API with `split_by_month` logic, and update E2E/admin tests with session mocking and department sync fixes. Normalize POS department handling, improve invoicing period split logic, and enhance receipt export safety. --- .../buttons/ActionSettingsWheelButton.vue | 42 +- .../displays/buttons/DatePeriodSelector.vue | 75 ++- .../PosDesktopOrderBookingSelectorModal.vue | 1 + .../PosDepartmentStepMobile2Product.vue | 11 +- .../PosDepartmentStepMobileButtonNextStep.vue | 1 + ...partmentStepMobilePopupCompleteBooking.vue | 7 - ...tmentStepMobilePopupSelectOrderBooking.vue | 1 + .../forms/auth/CustomerRegistrationForm.vue | 1 + .../items/NavigationMenuItemsAdmin.vue | 3 +- src/components/session/token/SessionUser.vue | 4 +- .../Objects/CollectedOrderInvoices.vue | 13 + .../SessionUser/Objects/OrderBookings.vue | 37 -- src/components/shop/POSDepartmentProcess.vue | 111 +++- .../edgeGateways/EdgeGatewayManagePage.vue | 24 +- .../edgeGateways/EdgeGatewaySettingsPage.vue | 24 +- src/i18n/locales/da.json | 20 + src/i18n/locales/de.json | 72 ++- src/i18n/locales/en.json | 20 + src/i18n/locales/no.json | 54 ++ src/i18n/locales/sv.json | 72 ++- .../modules/Pos/DepartmentPosOrder.vue | 21 +- .../DepartmentBookingCertificateOverview.vue | 30 +- ...partmentDashboardDailyReportNavigation.vue | 17 +- .../self-serve/DepartmentSelfServeStudio.vue | 11 +- ...voicingBillingPeriodDatePeriodSelector.vue | 140 +++++ .../InvoicingBillingPeriodStatistics.vue | 52 +- .../displays/layout/Right.vue | 4 +- .../InvoicingBillingPeriodImportDates.vue | 11 +- .../views/InvoicingBillingPeriodViewAll.vue | 8 +- .../displays/tables/OrderBookingsTable.vue | 10 - src/views/guest/qr/GuestQRNewCustomer.vue | 496 ++++-------------- tests/e2e/admin-bookings-mobile.spec.ts | 3 + tests/e2e/admin-pos-drafts.spec.ts | 2 - tests/e2e/admin-pos-orders.spec.ts | 87 ++- tests/e2e/adminModuleGoals.spec.ts | 113 ++-- tests/e2e/economic-queue-workflow.spec.js | 7 +- ...n-desktop-win32-chromium-desktop-win32.png | Bin 0 -> 24221 bytes ...g-desktop-win32-chromium-desktop-win32.png | Bin 0 -> 78548 bytes .../assign-draft-order-customer-modal.html | 1 - .../assign-draft-order-customer-modal.ts | 32 +- tests/e2e/i18n.views.spec.ts | 3 +- tests/e2e/invoicing-period.smoke.spec.js | 155 +++++- tests/e2e/pos-customer-rules.spec.js | 16 +- tests/e2e/pos-desktop-card-payments.spec.js | 33 +- tests/e2e/pos-flow.spec.js | 11 +- tests/e2e/pos-mobile-order-flow.spec.js | 49 +- tests/e2e/pos.visual.spec.js | 102 +++- ...-1-fixed-actions-chromium-mobile-win32.png | Bin 6896 -> 6999 bytes ...p-1-fixed-actions-firefox-mobile-win32.png | Bin 0 -> 7268 bytes ...os-mobile-step-2-chromium-mobile-win32.png | Bin 72882 -> 69685 bytes ...pos-mobile-step-2-firefox-mobile-win32.png | Bin 0 -> 55272 bytes .../pos-mobile-step-2-webkit-mobile-win32.png | Bin 41777 -> 37097 bytes ...os-mobile-step-3-chromium-mobile-win32.png | Bin 38851 -> 40421 bytes ...pos-mobile-step-3-firefox-mobile-win32.png | Bin 0 -> 33759 bytes .../pos-mobile-step-3-webkit-mobile-win32.png | Bin 29107 -> 27928 bytes ...d-items-desktop-chromium-desktop-win32.png | Bin 62043 -> 61944 bytes ...dd-items-desktop-firefox-desktop-win32.png | Bin 69193 -> 69853 bytes ...add-items-desktop-webkit-desktop-win32.png | Bin 55190 -> 55069 bytes ...add-items-mobile-chromium-mobile-win32.png | Bin 17404 -> 17579 bytes ...-add-items-mobile-firefox-mobile-win32.png | Bin 0 -> 19611 bytes ...l-add-items-mobile-webkit-mobile-win32.png | Bin 17292 -> 17263 bytes ...-detail-desktop-chromium-desktop-win32.png | Bin 50727 -> 79400 bytes ...r-detail-desktop-firefox-desktop-win32.png | Bin 59601 -> 81230 bytes ...quired-warnings-chromium-desktop-win32.png | Bin 77504 -> 78489 bytes ...equired-warnings-firefox-desktop-win32.png | Bin 82904 -> 83225 bytes ...required-warnings-webkit-desktop-win32.png | Bin 69887 -> 70323 bytes ...er-detail-desktop-webkit-desktop-win32.png | Bin 47096 -> 71050 bytes ...ustomer-desktop-chromium-desktop-win32.png | Bin 50954 -> 69110 bytes ...customer-desktop-firefox-desktop-win32.png | Bin 57753 -> 71998 bytes ...-customer-desktop-webkit-desktop-win32.png | Bin 45892 -> 59564 bytes ...-step-1-desktop-chromium-desktop-win32.png | Bin 36588 -> 36469 bytes ...p-expanded-scan-chromium-desktop-win32.png | Bin 58116 -> 67702 bytes ...op-expanded-scan-firefox-desktop-win32.png | Bin 58342 -> 69558 bytes ...top-expanded-scan-webkit-desktop-win32.png | Bin 47442 -> 56113 bytes ...s-step-1-desktop-firefox-desktop-win32.png | Bin 42306 -> 42462 bytes ...os-step-1-desktop-webkit-desktop-win32.png | Bin 33284 -> 33205 bytes ...-step-2-desktop-chromium-desktop-win32.png | Bin 55373 -> 53995 bytes ...s-step-2-desktop-firefox-desktop-win32.png | Bin 62735 -> 59975 bytes ...os-step-2-desktop-webkit-desktop-win32.png | Bin 48927 -> 47900 bytes tests/e2e/qr-new-customer-layout.spec.ts | 48 ++ tests/e2e/self-serve-studio-flow.spec.js | 32 +- tests/e2e/superuser-bookings.spec.ts | 21 +- tests/e2e/userBookWash.spec.ts | 35 +- .../unit/action-settings-wheel-button.spec.js | 18 +- tests/unit/date-period-selector.spec.js | 57 +- tests/unit/pos-department-process.spec.js | 87 +++ vite.config.js | 17 +- 87 files changed, 1524 insertions(+), 798 deletions(-) create mode 100644 tests/e2e/edge-gateways.visual.spec.js-snapshots/edge-agents-navigation-desktop-win32-chromium-desktop-win32.png create mode 100644 tests/e2e/edge-gateways.visual.spec.js-snapshots/edge-gateways-hardware-fleet-landing-desktop-win32-chromium-desktop-win32.png create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-mobile-step-1-fixed-actions-firefox-mobile-win32.png create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-mobile-step-2-firefox-mobile-win32.png create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-mobile-step-3-firefox-mobile-win32.png create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-order-detail-add-items-mobile-firefox-mobile-win32.png create mode 100644 tests/e2e/qr-new-customer-layout.spec.ts diff --git a/src/components/displays/buttons/ActionSettingsWheelButton.vue b/src/components/displays/buttons/ActionSettingsWheelButton.vue index 663beddd..b015d04f 100644 --- a/src/components/displays/buttons/ActionSettingsWheelButton.vue +++ b/src/components/displays/buttons/ActionSettingsWheelButton.vue @@ -86,6 +86,10 @@ const props = defineProps({ type: Boolean, default: false, }, + allowBookingCompletion: { + type: Boolean, + default: false, + }, department_lane_id: { type: Number, default: null, @@ -1211,20 +1215,46 @@ const openPrimaryGatewayPage = async () => { SessionUser.functions.redirectTo.superUser("/selfserve/edge-agents", true); }; +const showCompleteOrderBookingConfirmation = async () => { + const result = await Swal.fire({ + title: t("bookings.complete_booking"), + text: t("pos.confirm_complete_order"), + icon: "warning", + showDenyButton: true, + showCancelButton: true, + confirmButtonText: t("admin.pos.settings_wheel.view_booking_new_tab"), + denyButtonText: t("tables.bookings.complete_wash_without_certificate"), + cancelButtonText: t("global.cancel"), + reverseButtons: true, + }); + + if (result.isConfirmed) { + SessionUser.functions.redirectTo.department( + props.department_id, + "modules/bookings/order/" + props.order_booking_id, + true + ); + return; + } + + if (result.isDenied) { + await SessionUser.objects.order_bookings.functions.complete(props.order_booking_id, null, () => { + props.refreshFunction(); + }); + } +}; + const flatBuiltInMenuSections = computed(() => { const sections = []; if (props.order_booking_id) { const bookingSection = buildMenuSection("booking", t("admin.pos.settings_wheel.booking"), [ - (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()) && !props.order_id + props.allowBookingCompletion ? buildMenuAction("booking-complete", { - icon: "fas fa-check-circle", + icon: "fas fa-check", label: t("admin.pos.settings_wheel.mark_as_completed"), template: "success", - clickAction: () => - SessionUser.objects.order_bookings.functions.showCompleteConfirmationModal(props.order_booking_id, () => { - props.refreshFunction(); - }), + clickAction: showCompleteOrderBookingConfirmation, }) : null, buildMenuAction("booking-view", { diff --git a/src/components/displays/buttons/DatePeriodSelector.vue b/src/components/displays/buttons/DatePeriodSelector.vue index 156ac78c..8c80534f 100644 --- a/src/components/displays/buttons/DatePeriodSelector.vue +++ b/src/components/displays/buttons/DatePeriodSelector.vue @@ -119,9 +119,25 @@ const formatDateInputValue = (date: Date) => { return ""; } - return date.toISOString().split("T")[0]; + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; }; +const parseDateInputValue = (value: string) => { + const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (!match) { + return new Date(value); + } + + return new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); +}; + +const isSameDateInputValue = (left: Date, right: Date) => ( + formatDateInputValue(left) === formatDateInputValue(right) +); + const updateSelection = () => { emits("update:selection", { startDate: startDate.value, endDate: endDate.value }); props.onSelectionChange(startDate.value, endDate.value); @@ -177,25 +193,38 @@ const isSelectionValid = computed(() => ( )); const handleStartDateChange = (event) => { - const newDate = new Date(event.target.value); + const newDate = parseDateInputValue(event.target.value); if (Number.isNaN(newDate.getTime())) { console.warn("Invalid start date:", event.target.value); return; } + if (isSameDateInputValue(newDate, startDate.value)) { + return; + } + applyDateRange(newDate, endDate.value); }; const handleEndDateChange = (event) => { - const newDate = new Date(event.target.value); + const newDate = parseDateInputValue(event.target.value); if (Number.isNaN(newDate.getTime())) { console.warn("Invalid end date:", event.target.value); return; } + if (isSameDateInputValue(newDate, endDate.value)) { + return; + } + applyDateRange(startDate.value, newDate); }; +const getFullMonthRange = (year: number, month: number): DateRange => ({ + startDate: new Date(year, month - 1, 1, 0, 0, 0, 0), + endDate: new Date(year, month, 0, 23, 59, 59, 999), +}); + const setMonth = (month: number) => { if (month < 1 || month > 12) { console.warn("Invalid month selection:", month); @@ -203,9 +232,8 @@ const setMonth = (month: number) => { } const year = startDate.value.getFullYear(); - const startOfMonth = new Date(year, month - 1, 1); - const endOfMonth = new Date(year, month, 0); - applyDateRange(startOfMonth, endOfMonth); + const range = getFullMonthRange(year, month); + applyDateRange(range.startDate, range.endDate); }; const handleMonthChange = (event) => { @@ -217,6 +245,10 @@ const handleMonthChange = (event) => { setMonth(month); }; +const setCurrentSelectionEntireMonth = () => { + setMonth(currentMonthSelection.value); +}; + const handleYearChange = (event) => { const year = parseInt(event.target.value, 10); if (Number.isNaN(year)) { @@ -307,24 +339,14 @@ const shortcuts = computed(() => { label: SessionUser.objects.global.language.text.this_month, getRange: () => { const today = new Date(); - const firstDayOfThisMonth = new Date(today.getFullYear(), today.getMonth(), 1); - const lastDayOfThisMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); - return { - startDate: new Date(firstDayOfThisMonth.setHours(23, 59, 59, 999)), - endDate: new Date(lastDayOfThisMonth.setHours(23, 59, 59, 999)), - }; + return getFullMonthRange(today.getFullYear(), today.getMonth() + 1); }, }, { label: SessionUser.objects.global.language.text.last_month, getRange: () => { const today = new Date(); - const firstDayOfLastMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1); - const lastDayOfLastMonth = new Date(today.getFullYear(), today.getMonth(), 0); - return { - startDate: new Date(firstDayOfLastMonth.setHours(23, 59, 59, 999)), - endDate: new Date(lastDayOfLastMonth.setHours(23, 59, 59, 999)), - }; + return getFullMonthRange(today.getFullYear(), today.getMonth()); }, }, { @@ -346,16 +368,7 @@ const shortcuts = computed(() => { label: SessionUser.objects.global.language.text.same_month_last_year, getRange: () => { const today = new Date(); - const firstDayOfThisMonth = new Date(today.getFullYear(), today.getMonth(), 1); - const lastDayOfThisMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0); - const firstDayOfLastYearSameMonth = new Date(firstDayOfThisMonth); - firstDayOfLastYearSameMonth.setFullYear(firstDayOfThisMonth.getFullYear() - 1); - const lastDayOfLastYearSameMonth = new Date(lastDayOfThisMonth); - lastDayOfLastYearSameMonth.setFullYear(lastDayOfThisMonth.getFullYear() - 1); - return { - startDate: new Date(firstDayOfLastYearSameMonth.setHours(23, 59, 59, 999)), - endDate: new Date(lastDayOfLastYearSameMonth.setHours(23, 59, 59, 999)), - }; + return getFullMonthRange(today.getFullYear() - 1, today.getMonth() + 1); }, }, ]; @@ -419,6 +432,7 @@ const handleShortcutSelection = (event) => { class="input" data-testid="date-period-start" :value="formatDateInputValue(startDate)" + @input="handleStartDateChange" @change="handleStartDateChange" :disabled="props.isDisabled || props.isReadonly" :class="{ 'is-danger': !isSelectionValid }" @@ -434,6 +448,7 @@ const handleShortcutSelection = (event) => { class="input" data-testid="date-period-end" :value="formatDateInputValue(endDate)" + @input="handleEndDateChange" @change="handleEndDateChange" :disabled="props.isDisabled || props.isReadonly" :class="{ 'is-danger': !isSelectionValid }" @@ -516,6 +531,7 @@ const handleShortcutSelection = (event) => { class="input" data-testid="date-period-start" :value="formatDateInputValue(startDate)" + @input="handleStartDateChange" @change="handleStartDateChange" :disabled="props.isDisabled || props.isReadonly" :class="{ 'is-danger': !isSelectionValid }" @@ -530,6 +546,7 @@ const handleShortcutSelection = (event) => { class="input" data-testid="date-period-end" :value="formatDateInputValue(endDate)" + @input="handleEndDateChange" @change="handleEndDateChange" :disabled="props.isDisabled || props.isReadonly" :class="{ 'is-danger': !isSelectionValid }" @@ -648,7 +665,7 @@ const handleShortcutSelection = (event) => { iconPack="fas" has-icon > - Selection is not an entire month Click to set entire month. + Selection is not an entire month Click to set entire month. When a partial month selection is made, it may not accurately represent the intended time period. Consider selecting an entire month for more precise results. diff --git a/src/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue b/src/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue index ade007b7..1f00b75d 100644 --- a/src/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue +++ b/src/components/displays/department/pos/steps/elements/PosDesktopOrderBookingSelectorModal.vue @@ -413,6 +413,7 @@ const closeModal = () => { :refreshFunction="emitRefreshBookings" :icon="'fas fa-ellipsis-v'" trigger-button-variant="text" + allow-booking-completion > diff --git a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupSelectOrderBooking.vue b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupSelectOrderBooking.vue index 0d655f9a..3dd6f93c 100644 --- a/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupSelectOrderBooking.vue +++ b/src/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobilePopupSelectOrderBooking.vue @@ -355,6 +355,7 @@ const isBookingScheduledForToday = (booking: any) => { :refreshFunction="emitRefreshBookings" :icon="'fas fa-ellipsis-v'" trigger-button-variant="text" + allow-booking-completion > diff --git a/src/components/models/navigation/items/NavigationMenuItemsAdmin.vue b/src/components/models/navigation/items/NavigationMenuItemsAdmin.vue index 185e2577..b899852b 100644 --- a/src/components/models/navigation/items/NavigationMenuItemsAdmin.vue +++ b/src/components/models/navigation/items/NavigationMenuItemsAdmin.vue @@ -56,6 +56,7 @@ const department_booking_counts_loading = ref(false); const department_draft_count = ref(0); const department_draft_count_loading = ref(false); const draftTransactionCustomerNumber = computed(() => getDraftTransactionCustomerNumber()); +const canUseAdminNavigationCountsValue = computed(() => canUseAdminNavigationCounts()); let bookingCountRequestId = 0; let draftCountRequestId = 0; let bookingCountFetchInFlight = false; @@ -328,7 +329,7 @@ const isDepartmentSet = computed(() => { return department_id.value !== 'default' && parseInt(department_id.value) > 0; }); -watch(department_id, () => { +watch([department_id, canUseAdminNavigationCountsValue], () => { refreshNavigationCounts({ showBookingLoadingIndicator: true, showDraftLoadingIndicator: true, diff --git a/src/components/session/token/SessionUser.vue b/src/components/session/token/SessionUser.vue index 224b1ff8..53fd79b5 100644 --- a/src/components/session/token/SessionUser.vue +++ b/src/components/session/token/SessionUser.vue @@ -749,8 +749,10 @@ export const SessionUser = { // If // If the current path is not an admin path, redirect to the department page const newPath = currentPath.replace(/\/admin\/\d+/, '').replace(/^\/admin\/?/, '').replace(/^\//, ''); + const shouldPreserveCurrentQuery = newPath === 'modules/pos' || newPath.startsWith('modules/pos/'); + const queryString = shouldPreserveCurrentQuery ? window.location.search : ''; // Redirect to the department page - SessionUser.functions.redirectTo.department(departmentId, newPath); + SessionUser.functions.redirectTo.department(departmentId, newPath + queryString); }, /** Show confirm logout dialog */ showConfirmLogoutDialog: () => { diff --git a/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue b/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue index 85df9215..fa5116c4 100644 --- a/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue +++ b/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue @@ -440,6 +440,19 @@ export const CollectedOrderInvoices = { throw error; }); }, + split_by_month: async (dateFrom, dateTo, options = {}) => { + return authenticatedRequest('/collected-invoices/split-by-month', 'POST', { + dateFrom, + dateTo, + ...(options.preview !== undefined ? { preview: !!options.preview } : {}), + }).then((response) => { + console.log(response); + return response; + }).catch((error) => { + console.log(error); + throw error; + }); + }, showCreateNewCustom: showCreateCustomInvoiceCollectionForm, showInvoiceCollectionPickerForm: showInvoiceCollectionPickerModal, add_vehicle_subscriptions: async (id) => { diff --git a/src/components/session/token/SessionUser/Objects/OrderBookings.vue b/src/components/session/token/SessionUser/Objects/OrderBookings.vue index daaa9e01..35b0a381 100644 --- a/src/components/session/token/SessionUser/Objects/OrderBookings.vue +++ b/src/components/session/token/SessionUser/Objects/OrderBookings.vue @@ -1,5 +1,4 @@