From 157c3dbf609eef65075f70a4e393bf6e45bc3a0a Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Thu, 9 Apr 2026 14:03:46 +0200 Subject: [PATCH] Add enhanced POS desktop workspace with customer rail and order summary: - Introduced `PosDesktopOrderWorkspace.vue` for improved order and customer management. - Added `PosOrderItemsDefaultTable.vue` with detailed row actions, price tooltips, and order item grouping. - Enhanced `LicensePlateReg1Input.vue` to preserve customer selection when switching license plates. - Improved `PosDepartmentStep3.vue` with updated desktop layout using the new POS workspace. - Added e2e test for customer rules tab to verify dynamic attribute reloading. --- src/assets/pos.css | 57 ++- .../displays/department/pos/PosNotes.vue | 6 +- .../department/pos/PosOrderItemsCurrent.vue | 164 ++++++-- .../department/pos/PosSelectedCustomer.vue | 57 ++- .../pos/order/PosDesktopOrderWorkspace.vue | 274 +++++++++++++ .../pos/order/PosOrderDetailItemsTable.vue | 55 ++- .../pos/order/PosOrderItemsDefaultTable.vue | 266 +++++++++++++ .../pos/steps/PosDepartmentStep2.vue | 93 ++--- .../pos/steps/PosDepartmentStep3.vue | 108 +++-- .../pos/steps/PosDepartmentStep4.vue | 61 ++- .../PosDepartmentStepMobileAttachment.vue | 40 +- .../pos/input/LicensePlateReg1Input.vue | 101 +++-- .../token/SessionUser/Objects/Orders.vue | 71 ++-- src/components/shop/POSDepartmentProcess.vue | 153 ++++++-- src/i18n/locales/da.json | 5 + src/i18n/locales/de.json | 5 + src/i18n/locales/en.json | 5 + src/i18n/locales/no.json | 5 + src/i18n/locales/sv.json | 5 + src/i18n/locales/sv_backup.json | 3 + .../modules/Pos/DepartmentPosOrder.vue | 370 ++++++++---------- ...partmentDashboardDailyReportComplaints.vue | 112 +++++- tests/e2e/admin-daily-report.spec.ts | 15 +- tests/e2e/admin-pos-orders.spec.ts | 175 +++++++++ tests/e2e/pos-customer-rules.spec.js | 68 ++++ tests/e2e/pos-flow.spec.js | 14 + tests/e2e/pos.visual.spec.js | 172 ++++++++ ...d-items-desktop-chromium-desktop-win32.png | Bin 64697 -> 63159 bytes ...-detail-desktop-chromium-desktop-win32.png | Bin 48597 -> 48077 bytes ...-step-2-desktop-chromium-desktop-win32.png | Bin 0 -> 51365 bytes tests/e2e/support/network.js | 91 ++++- ...department-daily-report-complaints.spec.js | 17 +- 32 files changed, 2030 insertions(+), 538 deletions(-) create mode 100644 src/components/displays/department/pos/order/PosDesktopOrderWorkspace.vue create mode 100644 src/components/displays/department/pos/order/PosOrderItemsDefaultTable.vue create mode 100644 tests/e2e/pos-customer-rules.spec.js create mode 100644 tests/e2e/pos.visual.spec.js-snapshots/pos-step-2-desktop-chromium-desktop-win32.png diff --git a/src/assets/pos.css b/src/assets/pos.css index ec71eace..b2ce9807 100644 --- a/src/assets/pos.css +++ b/src/assets/pos.css @@ -99,6 +99,35 @@ color: #21446d; } +.tabs.pos-sidebar-panel-tabs { + margin: 0 0 0.35rem; +} + +.tabs.pos-sidebar-panel-tabs ul { + justify-content: flex-start; + margin: 0; + gap: 0.35rem; + flex-wrap: wrap; + border-bottom: none; +} + +.tabs.pos-sidebar-panel-tabs li a { + display: inline-flex; + align-items: center; + gap: 0.45rem; + border: none; + border-radius: 999px; + min-height: 2.1rem; + padding: 0.45rem 0.85rem; + color: #5a6980; + font-weight: 600; +} + +.tabs.pos-sidebar-panel-tabs li.is-active a { + background: #eef4ff; + color: #21446d; +} + .buttons.pos-actions { margin: 0; gap: 0.75rem; @@ -139,19 +168,31 @@ display: flex; flex-wrap: wrap; align-items: center; - gap: 0.5rem; + gap: 0.85rem; } .pos-inline-tag { display: inline-flex; - align-items: center; - padding: 0.35rem 0.7rem; - border-radius: 999px; - background: #f3f7fc; - color: #43566f; - font-size: 0.78rem; + align-items: baseline; + gap: 0.35rem; + padding: 0; + border-radius: 0; + background: transparent; + color: #5c6d83; + font-size: var(--pos-order-subheading-font-size, 0.75rem); + font-weight: 500; + line-height: var(--pos-order-subheading-line-height, 1.5); + white-space: nowrap; +} + +.pos-inline-tag__label { + color: #7f8ea3; + font-weight: 600; +} + +.pos-inline-tag__value { + color: #243347; font-weight: 600; - line-height: 1; } .pos-desktop-step { diff --git a/src/components/displays/department/pos/PosNotes.vue b/src/components/displays/department/pos/PosNotes.vue index bbf373e8..28f44679 100644 --- a/src/components/displays/department/pos/PosNotes.vue +++ b/src/components/displays/department/pos/PosNotes.vue @@ -18,8 +18,8 @@ showLabel.value = props.isLabelVisible || false;
-
-
+
+

{{ note.created_at }}

@@ -50,4 +50,4 @@ showLabel.value = props.isLabelVisible || false; \ No newline at end of file + diff --git a/src/components/displays/department/pos/PosOrderItemsCurrent.vue b/src/components/displays/department/pos/PosOrderItemsCurrent.vue index 46382ca7..a39750c4 100644 --- a/src/components/displays/department/pos/PosOrderItemsCurrent.vue +++ b/src/components/displays/department/pos/PosOrderItemsCurrent.vue @@ -4,6 +4,7 @@ import { showEditOrderItemForm } from "@/components/forms/superUser/editOrderIte import { removeOrderItem } from "@/components/shop/OrdersItems.vue"; import { computed, ref, watch, defineProps, useSlots, defineEmits } from 'vue'; import PosOrderDetailItemsTable from "@/components/displays/department/pos/order/PosOrderDetailItemsTable.vue"; +import PosOrderItemsDefaultTable from "@/components/displays/department/pos/order/PosOrderItemsDefaultTable.vue"; import PosOrderItemEditModal from "@/components/displays/department/pos/order/PosOrderItemEditModal.vue"; const props = defineProps({ orderItems: Array, @@ -20,7 +21,7 @@ const props = defineProps({ variant: { type: String, default: 'default', - validator: (value) => ['default', 'order-detail'].includes(value), + validator: (value) => ['default', 'order-detail', 'sidebar'].includes(value), }, }); const emits = defineEmits(['update:orderItems', 'onDestroyItem']); @@ -45,21 +46,24 @@ const panel_tabs = ref([ name: t('admin.pos.cart'), active: true, }, - { - id: 'attachments', - icon: 'fas fa-paperclip', - name: t('admin.pos.attachments'), - active: false, - } ]); // Check if the size is large const isLarge = props.isLarge !== undefined ? props.isLarge : false; const isOrderDetailVariant = computed(() => props.variant === 'order-detail'); +const isSidebarVariant = computed(() => props.variant === 'sidebar'); const showInlineActionButtons = computed(() => isOrderDetailVariant.value); +const showSidebarTabsInHeader = computed(() => isSidebarVariant.value && panel_tabs.value.length > 1); const rootClasses = computed(() => ({ 'pos-order-items': true, 'pos-order-items--large': isLarge, 'pos-order-items--order-detail': isOrderDetailVariant.value, + 'pos-order-items--sidebar': isSidebarVariant.value, +})); +const titleClasses = computed(() => ({ + title: true, + 'is-5': !isSidebarVariant.value, + 'is-6': isSidebarVariant.value, + 'pos-order-items__title': true, })); const displayCustomerName = computed(() => props.customer_name || customer_name.value || ''); const showHeaderActions = computed(() => slots.headerActions !== undefined); @@ -111,6 +115,16 @@ const orderDetailRows = computed(() => { // Check if the other slot is present const hasOtherSlot = slots.other !== undefined; +const hasAttachmentsSlot = slots.attachments !== undefined; + +if (hasAttachmentsSlot) { + panel_tabs.value.push({ + id: 'attachments', + icon: 'fas fa-paperclip', + name: t('admin.pos.attachments'), + active: false, + }); +} // If so, add the other tab if (hasOtherSlot) { @@ -134,27 +148,23 @@ if (hasDetailsSlot) { }); } -const getItemRelatedItems = (orderItems, orderItemId) => { - // Return all items with the item.related_item_id equal to the orderItemId - return (orderItems || []).filter((orderItem) => orderItem.related_item_id === orderItemId); -}; - const getOrderItemProductName = (orderItem) => { return orderItem?.product?.name || t('global.no_data'); }; +const getItemRelatedItems = (orderItems, orderItemId) => { + return (orderItems || []).filter((orderItem) => orderItem.related_item_id === orderItemId); +}; + const getOrderItemBasePrice = (orderItem) => { return Number(orderItem?.product?.price ?? orderItem?.price ?? 0); }; const isLastRelatedItem = (orderItems, orderItem) => { - // Get all related items const relatedItems = getItemRelatedItems(orderItems, orderItem.related_item_id); - // If there are no related items, return true if (relatedItems.length === 0) { return true; } - // If the last related item is the current item, return true return relatedItems[relatedItems.length - 1].id === orderItem.id; }; @@ -178,6 +188,10 @@ const parseOrderId = (candidateOrderId) => { return normalizedOrderId; }; +const activeOrderId = computed(() => { + return parseOrderId(props.orderId) ?? parseOrderId(order_id.value); +}); + const syncRegistrationNumbers = (orderDetails = null) => { const reg_numbers = registration_numbers.value; reg_numbers[1] = normalizeRegistrationValue(orderDetails?.reg_1); @@ -185,7 +199,7 @@ const syncRegistrationNumbers = (orderDetails = null) => { reg_numbers[3] = normalizeRegistrationValue(orderDetails?.reg_3); }; -const loadOrder = (candidateOrderId = props.orderId) => { +const loadOrder = (candidateOrderId = activeOrderId.value ?? props.orderId) => { const normalizedOrderId = parseOrderId(candidateOrderId); if (!normalizedOrderId) { registrationNumbersLoading.value = false; @@ -279,6 +293,12 @@ const closeOrderItemEditor = () => { selectedOrderItem.value = null; }; +const activateTab = (selectedTab) => { + panel_tabs.value.forEach((tab) => { + tab.active = tab === selectedTab; + }); +}; + const openOrderItemEditor = (orderItem) => { if (isOrderDetailVariant.value) { selectedOrderItem.value = { @@ -341,7 +361,7 @@ const deleteOrderItem = async (orderItemId) => {
{ {{ displayCustomerName }}
-

{{ $t('admin.pos.transaction') }} #{{ orderId }}

+

+ {{ $t('admin.pos.transaction') }} #{{ orderId }} +

@@ -370,15 +395,35 @@ const deleteOrderItem = async (orderItemId) => {
+ -