From 04467316c273b1fd040d323e35c1caa9cf2e240e Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Thu, 23 Apr 2026 18:45:01 +0200 Subject: [PATCH] Add gateway health handling to unit and E2E tests, refine department lanes table logic, and enhance advanced view behavior. --- .../buttons/ActionSettingsWheelButton.vue | 1216 ++++++++++------- .../buttons/ActionSettingsWheelToggleItem.vue | 133 ++ .../department/pos/PosSelectedCustomer.vue | 88 +- .../system/SystemStatusDashboard.vue | 459 ++++++- .../superuser/tables/departmentLanesTable.vue | 564 ++++++-- .../superuser/tables/productsTable.vue | 631 ++++++--- src/components/shop/CustomerAttributes.vue | 44 +- .../customer/customerAttributeService.js | 111 ++ src/features/customer/customerRuleRegistry.js | 127 ++ .../EdgeGatewayDepartmentWorkspace.vue | 225 ++- src/i18n/locales/da.json | 134 +- src/i18n/locales/de.json | 114 ++ src/i18n/locales/en.json | 136 +- src/i18n/locales/no.json | 114 ++ src/i18n/locales/sv.json | 114 ++ src/i18n/locales/sv_backup.json | 16 + tests/e2e/admin-pos-orders.spec.ts | 56 + tests/e2e/edge-gateways.routes.spec.js | 25 + tests/e2e/edge-gateways.smoke.spec.js | 128 ++ tests/e2e/superuser-department-lanes.spec.ts | 147 +- tests/e2e/superuser-products-layout.spec.ts | 291 ++++ .../e2e/superuser-system-status.smoke.spec.js | 196 ++- tests/e2e/support/network.js | 283 +++- .../unit/action-settings-wheel-button.spec.js | 389 +++++- tests/unit/i18n-action-settings-wheel.spec.js | 77 ++ tests/unit/setup.js | 50 +- .../superuser-system-status-dashboard.spec.js | 294 +++- tmp-lanes-preview.pid | 1 + vitest.config.js | 1 - 29 files changed, 5106 insertions(+), 1058 deletions(-) create mode 100644 src/components/displays/buttons/ActionSettingsWheelToggleItem.vue create mode 100644 src/features/customer/customerAttributeService.js create mode 100644 src/features/customer/customerRuleRegistry.js create mode 100644 tests/e2e/superuser-products-layout.spec.ts create mode 100644 tmp-lanes-preview.pid diff --git a/src/components/displays/buttons/ActionSettingsWheelButton.vue b/src/components/displays/buttons/ActionSettingsWheelButton.vue index 6126a2aa..bd6592b0 100644 --- a/src/components/displays/buttons/ActionSettingsWheelButton.vue +++ b/src/components/displays/buttons/ActionSettingsWheelButton.vue @@ -3,11 +3,21 @@ import { onMounted, onBeforeUnmount, computed, nextTick, watch } from "vue"; import { useSlots } from "vue"; import { SessionUser } from "@/components/session/token/SessionUser.vue"; import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue"; +import ActionSettingsWheelToggleItem from "@/components/displays/buttons/ActionSettingsWheelToggleItem.vue"; import Swal from "sweetalert2"; import { ref } from "vue"; import CustomerModal from "@/components/displays/modals/CustomerModal.vue"; import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue"; import { useI18n } from "vue-i18n"; +import { API_URL } from "@/config.js"; +import { getCustomerRuleDefinitions } from "@/features/customer/customerRuleRegistry.js"; +import { + buildCustomerAttributeTargetPayload, + createCustomerAttribute, + deleteCustomerAttribute, + extractCustomerAttributesData, + listCustomerAttributes, +} from "@/features/customer/customerAttributeService.js"; const { t } = useI18n(); const slots = useSlots(); @@ -89,6 +99,10 @@ const props = defineProps({ const customerModalVisible = ref(false); const userIdFromCustomerNumber = ref(null); const userLookupRequestId = ref(0); +const isResolvingUserId = ref(false); +const customerRuleAttributes = ref([]); +const customerRuleAttributesLoaded = ref(false); +const customerRuleAttributesLoading = ref(false); const dropdownRoot = ref(null); const dropdownContent = ref(null); const isDropdownOpen = ref(false); @@ -284,17 +298,20 @@ const onViewportChange = () => { const resolveUserId = async () => { if (props.user_id) { + isResolvingUserId.value = false; userIdFromCustomerNumber.value = props.user_id; return; } if (!props.customer_number) { + isResolvingUserId.value = false; userIdFromCustomerNumber.value = null; return; } const currentLookupId = userLookupRequestId.value + 1; userLookupRequestId.value = currentLookupId; + isResolvingUserId.value = true; try { const response = await SessionUser.adminUser.customers.fromCustomerNumber.getUserId(props.customer_number); @@ -310,34 +327,67 @@ const resolveUserId = async () => { userIdFromCustomerNumber.value = null; console.error(error); + } finally { + if (currentLookupId === userLookupRequestId.value) { + isResolvingUserId.value = false; + } } }; -watch( - () => [props.user_id, props.customer_number], - () => { - void resolveUserId(); - }, - { immediate: true } -); +const resetCustomerRuleState = () => { + customerRuleAttributes.value = []; + customerRuleAttributesLoaded.value = false; + customerRuleAttributesLoading.value = false; +}; -watch(isDropdownOpen, async (isOpen) => { - if (!isOpen) { - isDesktopFlyoutLayout.value = false; - activeDesktopFlyoutSectionKey.value = null; - clearAttachmentPreviewState(); - resetDropdownLayout(); +const loadCustomerRuleAttributes = async ({ force = false } = {}) => { + if (!canViewCustomerRules.value || !customerAttributeTarget.value) { + return customerRuleAttributes.value; + } + + if (!force && (customerRuleAttributesLoaded.value || customerRuleAttributesLoading.value)) { + return customerRuleAttributes.value; + } + + customerRuleAttributesLoading.value = true; + + try { + const response = await listCustomerAttributes(customerAttributeTarget.value); + customerRuleAttributes.value = extractCustomerAttributesData(response); + customerRuleAttributesLoaded.value = true; + } catch (error) { + console.warn("Unable to load customer attributes for action wheel", error); + } finally { + customerRuleAttributesLoading.value = false; + } + + return customerRuleAttributes.value; +}; + +const toggleCustomerRuleAttribute = async (attribute, enabled) => { + if (!customerAttributeTarget.value || !attribute) { return; } - await nextTick(); + if (enabled) { + await createCustomerAttribute(customerAttributeTarget.value, attribute); - if (dropdownContent.value) { - dropdownContent.value.scrollTop = 0; + if (!activeCustomerRuleAttributes.value.has(attribute)) { + customerRuleAttributes.value = [ + ...customerRuleAttributes.value, + { + attribute, + ...customerAttributeTarget.value, + }, + ]; + } + + return; } - void updateDropdownLayout(); -}); + await deleteCustomerAttribute(customerAttributeTarget.value, attribute); + customerRuleAttributes.value = customerRuleAttributes.value.filter((entry) => entry?.attribute !== attribute); +}; const showSetCustomerPassword = (userId) => { Swal.fire({ @@ -398,6 +448,132 @@ const redirectDepartmentOrderPage = async (orderId, newTab = false) => { }; const hasUser = computed(() => Boolean(props.user_id || props.customer_number || userIdFromCustomerNumber.value)); +const customerAttributeTarget = computed(() => { + if (!hasUser.value) { + return null; + } + + const payload = buildCustomerAttributeTargetPayload({ + userId: props.user_id, + customerNumber: props.customer_number, + }); + + return Object.keys(payload).length > 0 ? payload : null; +}); +const canViewCustomerRules = computed( + () => hasUser.value && SessionUser.canAccessSuperUser() && SessionUser.hasPermission("list_customer_attributes") +); +const customerRuleDefinitions = computed(() => getCustomerRuleDefinitions()); +const activeCustomerRuleAttributes = computed(() => { + const enabledAttributes = new Set(); + + customerRuleAttributes.value.forEach((entry) => { + const attributeKey = String(entry?.attribute || "").trim(); + if (attributeKey) { + enabledAttributes.add(attributeKey); + } + }); + + return enabledAttributes; +}); +const customerRuleItems = computed(() => + customerRuleDefinitions.value.map((rule) => { + const isEnabled = activeCustomerRuleAttributes.value.has(rule.attribute); + const requiredPermission = isEnabled ? rule.deletePermission : rule.addPermission; + + return { + attribute: rule.attribute, + description: t(rule.descriptionKey), + disabled: !SessionUser.hasPermission(requiredPermission), + key: `customer-rule-${rule.attribute}`, + label: t(rule.labelKey), + testId: `action-settings-wheel-toggle-customer-rule-${rule.attribute}`, + type: "toggle", + value: isEnabled, + }; + }) +); +const customerShortcutItems = computed(() => { + if (!SessionUser.canAccessSuperUser() || !userIdFromCustomerNumber.value) { + return []; + } + + const userId = userIdFromCustomerNumber.value; + return [ + { + key: "customer-shortcut-overview", + icon: "fas fa-user", + label: t("admin.pos.settings_wheel.shortcut_overview"), + clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}`, true), + }, + { + key: "customer-shortcut-orders", + icon: "fas fa-file-alt", + label: t("admin.pos.settings_wheel.shortcut_orders"), + clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/orders`, true), + }, + { + key: "customer-shortcut-pricing", + icon: "fas fa-tags", + label: t("admin.pos.settings_wheel.shortcut_pricing"), + clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/pricing`, true), + }, + { + key: "customer-shortcut-other", + icon: "fas fa-sliders-h", + label: t("admin.pos.settings_wheel.shortcut_other"), + clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/other`, true), + }, + { + key: "customer-shortcut-vehicles", + icon: "fas fa-car", + label: t("admin.pos.settings_wheel.shortcut_vehicles"), + clickAction: () => SessionUser.functions.redirectTo.superUser(`/users/${userId}/vehicles`, true), + }, + ]; +}); + +watch( + () => [props.user_id, props.customer_number], + () => { + resetCustomerRuleState(); + void resolveUserId(); + + if (props.displayActionsDirectly || isDropdownOpen.value) { + void loadCustomerRuleAttributes({ force: true }); + } + }, + { immediate: true } +); + +watch( + () => props.displayActionsDirectly, + (displayActionsDirectly) => { + if (displayActionsDirectly) { + void loadCustomerRuleAttributes(); + } + }, + { immediate: true } +); + +watch(isDropdownOpen, async (isOpen) => { + if (!isOpen) { + isDesktopFlyoutLayout.value = false; + activeDesktopFlyoutSectionKey.value = null; + clearAttachmentPreviewState(); + resetDropdownLayout(); + return; + } + + await nextTick(); + + if (dropdownContent.value) { + dropdownContent.value.scrollTop = 0; + } + + void loadCustomerRuleAttributes(); + void updateDropdownLayout(); +}); const attachmentsFromOrder = ref([]); const attachmentsFromOrderError = ref(null); @@ -467,6 +643,46 @@ const getAttachmentPreviewKind = (attachment) => { return "none"; }; +const getAttachmentPreviewPlaceholderIcon = (attachment) => { + const previewKind = getAttachmentPreviewKind(attachment); + + if (previewKind === "image") { + return "fas fa-image"; + } + + if (previewKind === "document" || previewKind === "office") { + return "fas fa-file-alt"; + } + + if (previewKind === "link") { + return "fas fa-link"; + } + + if (previewKind === "text") { + return "fas fa-align-left"; + } + + return "fas fa-paperclip"; +}; + +const getAttachmentPreviewPlaceholderLabel = (attachment) => { + const extension = getAttachmentExtension(attachment).replace(".", "").toUpperCase(); + if (extension) { + return extension; + } + + const previewKind = getAttachmentPreviewKind(attachment); + if (previewKind === "link") { + return "LINK"; + } + + if (previewKind === "text") { + return "TEXT"; + } + + return "FILE"; +}; + const activeAttachment = computed(() => { return attachmentsFromOrder.value.find((attachment) => attachment.id === activeAttachmentId.value) || null; }); @@ -491,18 +707,69 @@ const hasCachedPreviewSource = (attachmentId) => { return Object.prototype.hasOwnProperty.call(previewSourcesById.value, attachmentId); }; +const buildAttachmentPreviewHeaders = () => { + const headers = {}; + const token = window.localStorage.getItem("token"); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + const isSubuser = window.localStorage.getItem("is_subuser") === "true"; + const selectedCustomerNumber = window.localStorage.getItem("selected_customer_number"); + if (isSubuser && selectedCustomerNumber) { + headers["X-Customer-Number"] = selectedCustomerNumber; + } + + return headers; +}; + +const shouldRetryPreviewRequestWithAuth = (downloadLink) => { + if (!downloadLink) { + return false; + } + + try { + const previewUrl = new URL(downloadLink, window.location.origin); + const apiOrigin = new URL(API_URL, window.location.origin).origin; + return previewUrl.origin === apiOrigin || previewUrl.origin === window.location.origin; + } catch { + return false; + } +}; + +const fetchAttachmentPreviewBlob = async (downloadLink, withAuthHeaders = false) => { + const requestConfig = { + method: "GET", + }; + + if (withAuthHeaders) { + requestConfig.headers = buildAttachmentPreviewHeaders(); + } + + const response = await fetch(downloadLink, requestConfig); + if (!response.ok) { + return null; + } + + const fileBlob = await response.blob(); + if (!fileBlob || fileBlob.size === 0) { + return null; + } + + return fileBlob; +}; + const createEmbeddablePreviewUrl = async (downloadLink) => { if (!downloadLink) { return null; } try { - const response = await fetch(downloadLink, { method: "GET" }); - if (!response.ok) { - return null; + let fileBlob = await fetchAttachmentPreviewBlob(downloadLink, false); + if (!fileBlob && shouldRetryPreviewRequestWithAuth(downloadLink)) { + fileBlob = await fetchAttachmentPreviewBlob(downloadLink, true); } - const fileBlob = await response.blob(); if (!fileBlob || fileBlob.size === 0) { return null; } @@ -688,20 +955,15 @@ const printAttachment = async (attachment) => { } const printableSource = (await ensurePreviewSource(attachment)) || downloadLink; - const printWindow = window.open("", "_blank", "noopener,noreferrer,width=960,height=720"); + if (previewKind === "image") { + const printWindow = window.open("", "_blank", "noopener,noreferrer,width=960,height=720"); + if (!printWindow) { + openAttachmentInNewTab(printableSource); + return; + } - if (!printWindow) { - openAttachmentInNewTab(printableSource); - return; - } - - const escapedTitle = getAttachmentLabel(attachment).replace(/"/g, """); - const contentMarkup = - previewKind === "image" - ? `` - : ``; - - printWindow.document.write(` + const escapedTitle = getAttachmentLabel(attachment).replace(/"/g, """); + printWindow.document.write(` ${escapedTitle} @@ -716,25 +978,61 @@ const printAttachment = async (attachment) => { align-items: center; justify-content: center; } - iframe { + img { display: block; + max-width: 100%; + max-height: 100vh; + object-fit: contain; } - ${contentMarkup} + + + `); - printWindow.document.close(); + printWindow.document.close(); - printWindow.addEventListener( - "load", - () => { - printWindow.focus(); - window.setTimeout(() => { - printWindow.print(); - }, 150); - }, - { once: true } - ); + const printableImage = printWindow.document.getElementById("attachment-print-image"); + if (!printableImage) { + openAttachmentInNewTab(printableSource); + return; + } + + printableImage.addEventListener( + "load", + () => { + printWindow.focus(); + window.setTimeout(() => { + printWindow.print(); + }, 150); + }, + { once: true } + ); + printableImage.addEventListener( + "error", + () => { + openAttachmentInNewTab(printableSource); + }, + { once: true } + ); + return; + } + + const printWindow = window.open(printableSource, "_blank", "noopener,noreferrer"); + if (!printWindow) { + openAttachmentInNewTab(printableSource); + return; + } + + const triggerPrint = () => { + printWindow.focus(); + window.setTimeout(() => { + printWindow.print(); + }, 250); + }; + + printWindow.addEventListener("load", triggerPrint, { once: true }); + window.setTimeout(triggerPrint, 900); } catch (error) { console.warn("Unable to print attachment", attachment?.id, error); Swal.fire({ @@ -838,350 +1136,21 @@ const onClickShowImpersonationQRCode = async (userId) => { onShowImpersonationQRCode(response.value, link); }; -const defaultActions = computed(() => { - return [ - /** - * Example action - { - icon: 'fas fa-eye', - label: 'View Order', - clickAction: () => { - console.warn('View Order clicked'); - }, - showFunction: () => { - return true; // Logic to determine if action should be shown - }, - disabled: false, - template: 'success' // Optional: Set the template to 'success' or 'danger' - isLabel: false // Optional: Set to true to render the label as a button - } - */ - /** Bookings actions */ - { - label: t("admin.pos.settings_wheel.booking"), - isLabel: true, - showFunction: () => { - return !!props.order_booking_id; - }, - }, - { - icon: "fas fa-check-circle", - label: t("admin.pos.settings_wheel.mark_as_completed"), - template: "success", - clickAction: () => { - return SessionUser.objects.order_bookings.functions.showCompleteConfirmationModal( - props.order_booking_id, - () => { - props.refreshFunction(); - } - ); - }, - showFunction: () => { - return !!props.order_booking_id && SessionUser.canAccessAdmin() && !props.order_id; - }, - disabled: false, - }, - { - icon: "fas fa-external-link-alt", - label: t("admin.pos.settings_wheel.view_booking_new_tab"), - clickAction: () => { - return SessionUser.functions.redirectTo.department( - props.department_id, - "modules/bookings/order/" + props.order_booking_id, - true - ); - }, - showFunction: () => { - return !!props.order_booking_id; - }, - }, - { - icon: "fas fa-edit", - label: t("admin.pos.settings_wheel.change_association"), - clickAction: () => { - return SessionUser.objects.order_bookings.showEditObjectFieldForm( - props.order_booking_id, - "order_id", - props.order_id, - () => { - props.refreshFunction(); - }, - { - filters: { - ...(props.customer_number ? { customer_id: props.customer_number } : {}), - ...(props.department_id ? { department_id: props.department_id } : {}), - }, - pagination: { - page: 1, - limit: 100, - }, - } - ); - }, - showFunction: () => { - return !!props.order_booking_id && SessionUser.canAccessAdmin(); - }, - disabled: false, - }, - { - icon: "fas fa-trash-alt", - label: t("admin.pos.settings_wheel.delete_booking"), - clickAction: () => { - return SessionUser.objects.order_bookings.functions.showDeleteConfirmationModal(props.order_booking_id, () => { - props.refreshFunction(); - }); - }, - showFunction: () => { - return !!props.order_booking_id && SessionUser.canAccessAdmin() && !props.order_id; - }, - disabled: false, - template: "danger", - }, - /** Department lane actions */ - { - label: SessionUser.objects.department_lanes.meta.labels.single, - isLabel: true, - showFunction: () => { - return !!props.department_lane_id; - }, - }, - { - icon: `fas fa-external-link-alt`, - label: t("admin.pos.settings_wheel.view_lane_new_tab"), - clickAction: () => { - return SessionUser.functions.redirectTo.superUser( - "/department/lanes/" + props.department_lane_id, - !SessionUser.functions.device.isMobile() - ); - }, - showFunction: () => { - return !!props.department_lane_id && SessionUser.canAccessSuperUser(); - }, - }, - { - icon: "fas fa-play", - label: t("superuser.department_lane.force_enable_machine"), - template: "success", - clickAction: () => { - return SessionUser.objects.department_lanes.functions.forceEnableMachine(props.department_lane_id); - }, - showFunction: () => { - return !!props.department_lane_id && SessionUser.canAccessSuperUser(); - }, - }, - { - icon: "fas fa-stop", - label: t("superuser.department_lane.force_disable_machine"), - template: "danger", - clickAction: () => { - return SessionUser.objects.department_lanes.functions.forceDisableMachine(props.department_lane_id); - }, - showFunction: () => { - return !!props.department_lane_id && SessionUser.canAccessSuperUser(); - }, - }, - /** Orders actions */ - { - label: SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.single), - isLabel: true, - showFunction: () => { - return !!props.order_id; - }, - }, - { - icon: "fas fa-external-link-alt", - label: t("admin.pos.settings_wheel.view_order_new_tab"), - clickAction: async () => { - return redirectDepartmentOrderPage(props.order_id, true); - }, - showFunction: () => { - return !!props.order_id && (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()); - }, - }, - { - icon: "fas fa-paperclip", - label: t("admin.pos.settings_wheel.attach_wash_certificate"), - clickAction: () => { - return SessionUser.objects.orders.functions.showAttachWashCertificateForm(props.order_id, () => { - props.refreshFunction(); - }); - }, - showFunction: () => { - return !!props.order_id && (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()); - }, - disabled: false, - }, - { - icon: "fas fa-user-edit", - label: t("admin.pos.settings_wheel.change_customer"), - clickAction: () => { - return SessionUser.objects.orders.functions.showChangeCustomerForm(props.order_id); - }, - showFunction: () => { - return !!props.order_id && SessionUser.canAccessSuperUser(); - }, - disabled: false, - }, - { - icon: "fas fa-file-invoice-dollar", - label: t("admin.pos.settings_wheel.change_invoice_collection"), - clickAction: () => { - return SessionUser.objects.orders.functions.showChangeInvoiceCollectionForm( - props.order_id, - props.refreshFunction - ); - }, - showFunction: () => { - return !!props.order_id && SessionUser.canAccessSuperUser(); - }, - disabled: false, - }, - { - icon: "fas fa-download", - label: t("admin.pos.settings_wheel.download_invoice"), - clickAction: () => { - return SessionUser.objects.collectedOrderInvoices.functions.download(props.invoice_collection_id); - }, - showFunction: () => { - return true; - }, - disabled: false, - }, - { - icon: "fas fa-trash-alt", - label: t("admin.pos.settings_wheel.delete_order"), - clickAction: () => { - return SessionUser.objects.orders.functions.showDeleteConfirmationModal(props.order_id, () => { - emitDeleted(); - }); - }, - showFunction: () => { - return !!props.order_id && (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()); - }, - disabled: false, - template: "danger", - }, - { - icon: "fas fa-file-invoice-dollar", - label: t("admin.pos.settings_wheel.view_invoice_collection_new_tab"), - clickAction: () => { - return redirectSuperUserInvoiceCollectionPage(props.invoice_collection_id); - }, - showFunction: () => { - return !!props.invoice_collection_id && SessionUser.canAccessSuperUser(); - }, - }, - /** Vehicle actions */ - { - label: - props.reg_1 && props.reg_2 - ? `${SessionUser.objects.vehicles.meta.labels.multiple}` - : `${SessionUser.objects.vehicles.meta.labels.single}`, - isLabel: true, - showFunction: () => { - return (!!props.reg_1 || !!props.reg_2) && SessionUser.canAccessSuperUser(); - }, - }, - { - icon: "fas fa-car", - label: t("admin.pos.settings_wheel.view_vehicle_new_tab", { reg: props.reg_1 ? props.reg_1 : "-" }), - clickAction: () => { - return SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_1, true); - }, - showFunction: () => { - return !!props.reg_1 && SessionUser.canAccessSuperUser(); - }, - }, - { - icon: "fas fa-car", - label: t("admin.pos.settings_wheel.view_vehicle_new_tab", { reg: props.reg_2 ? props.reg_2 : "-" }), - clickAction: () => { - return SessionUser.functions.redirectTo.superUser("/vehicles/" + props.reg_2, true); - }, - showFunction: () => { - return !!props.reg_2 && SessionUser.canAccessSuperUser(); - }, - }, - /** User actions */ - { - label: SessionUser.objects.global.language.customer, - isLabel: true, - showFunction: () => { - return hasUser.value && SessionUser.canAccessSuperUser(); - }, - }, - { - icon: "fas fa-user-edit", - label: t("admin.pos.settings_wheel.view_customer_new_tab"), - clickAction: () => { - return SessionUser.functions.redirectTo.superUser("/users/" + userIdFromCustomerNumber.value, true); - }, - showFunction: () => { - return hasUser.value && SessionUser.canAccessSuperUser() && userIdFromCustomerNumber.value; - }, - }, - { - icon: "fas fa-user-shield", - label: t("admin.pos.settings_wheel.login_as_customer"), - clickAction: () => { - return SessionUser.superUser.intimidate.intimidateUser(userIdFromCustomerNumber.value); - }, - showFunction: () => { - return hasUser.value && SessionUser.canAccessSuperUser(); - }, - }, - { - icon: "fas fa-qrcode", - label: t("admin.pos.settings_wheel.show_qr_code"), - clickAction: () => { - return onClickShowImpersonationQRCode(userIdFromCustomerNumber.value); - }, - showFunction: () => { - return hasUser.value && SessionUser.canAccessSuperUser(); - }, - }, - { - icon: "fas fa-key", - label: t("admin.pos.settings_wheel.change_password"), - clickAction: () => { - return showSetCustomerPassword(userIdFromCustomerNumber.value); - }, - showFunction: () => { - return hasUser.value && SessionUser.canAccessSuperUser(); - }, - }, - { - icon: "fas fa-user", - label: t("admin.pos.settings_wheel.show_customer"), - clickAction: () => { - customerModalVisible.value = true; - }, - showFunction: () => { - return hasUser.value && SessionUser.canAccessSuperUser(); - }, - }, - /** Subuser grant actions */ - { - icon: "fas fa-edit", - label: t("admin.pos.settings_wheel.edit_permissions"), - clickAction: () => { - return SessionUser.objects.subuser_grants.functions.showPermissionEditForm(props.subuserGrant, () => { - props.refreshFunction(); - }); - }, - showFunction: () => { - return !!props.subuserGrant && SessionUser.canAccessUser(); - }, - }, - ]; -}); - const hasCustomActionsSlot = computed(() => Boolean(slots.actions)); const buildMenuAction = (key, config) => ({ key, disabled: false, template: "default", + type: "action", + ...config, +}); + +const buildMenuToggleAction = (key, config) => ({ + key, + disabled: false, + type: "toggle", + value: false, ...config, }); @@ -1198,6 +1167,8 @@ const buildMenuSection = (key, label, items) => { }; }; +const isToggleMenuItem = (item) => item?.type === "toggle"; + const downloadOrderAttachment = (attachment) => downloadAttachment(attachment).catch(() => { Swal.fire({ @@ -1207,6 +1178,39 @@ const downloadOrderAttachment = (attachment) => }); }); +const openDepartmentWorkspaceTab = (tabId) => + SessionUser.functions.redirectTo.superUser( + `/departments/${props.department_id}/gateways?tab=${encodeURIComponent(String(tabId))}`, + true + ); + +const openPrimaryGatewayPage = async () => { + if (!props.department_id) { + SessionUser.functions.redirectTo.superUser("/configuration/edgegateway", true); + return; + } + + try { + const response = await SessionUser.request( + `/modules/edge-gateways/workspace/departments/${props.department_id}`, + "GET" + ); + const primaryGatewayId = response?.data?.data?.summary?.primary_gateway?.id ?? null; + + if (primaryGatewayId) { + SessionUser.functions.redirectTo.superUser( + `/configuration/edgegateway/${encodeURIComponent(String(primaryGatewayId))}/overview`, + true + ); + return; + } + } catch (error) { + console.warn("Unable to resolve primary gateway from department workspace", error); + } + + SessionUser.functions.redirectTo.superUser("/configuration/edgegateway", true); +}; + const flatBuiltInMenuSections = computed(() => { const sections = []; @@ -1409,47 +1413,238 @@ const flatBuiltInMenuSections = computed(() => { } } + if (props.department_lane_id && props.department_id) { + const selfServeStudioSection = buildMenuSection( + "department-self-serve-studio", + t("admin.pos.settings_wheel.self_serve_studio_section"), + [ + (SessionUser.canAccessAdmin() || SessionUser.canAccessSuperUser()) && props.department_id + ? buildMenuAction("department-self-serve-studio-open", { + icon: "fas fa-external-link-alt", + label: t("admin.pos.settings_wheel.open_studio"), + clickAction: () => + SessionUser.functions.redirectTo.department( + props.department_id, + "modules/self-serve/studio", + !SessionUser.functions.device.isMobile() + ), + }) + : null, + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-self-serve-studio-open-legacy", { + icon: "fas fa-external-link-alt", + label: t("admin.pos.settings_wheel.open_legacy_self_serve"), + clickAction: () => SessionUser.functions.redirectTo.superUser("/selfserve", true), + }) + : null, + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-self-serve-studio-open-workspace", { + icon: "fas fa-microchip", + label: t("admin.pos.settings_wheel.open_hardware_workspace_lanes"), + clickAction: () => openDepartmentWorkspaceTab("lanes"), + }) + : null, + ] + ); + + if (selfServeStudioSection) { + sections.push(selfServeStudioSection); + } + + const gatesSection = buildMenuSection("department-gates", t("admin.pos.settings_wheel.gates_section"), [ + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-gates-open-tab", { + icon: "fas fa-door-open", + label: t("admin.pos.settings_wheel.open_gates_tab"), + clickAction: () => openDepartmentWorkspaceTab("gates"), + }) + : null, + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-gates-open-legacy", { + icon: "fas fa-external-link-alt", + label: t("admin.pos.settings_wheel.open_legacy_gates"), + clickAction: () => SessionUser.functions.redirectTo.superUser("/department/gates", true), + }) + : null, + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-gates-create", { + icon: "fas fa-plus", + label: t("admin.pos.settings_wheel.add_gate"), + clickAction: () => + SessionUser.objects.department_gates.showCreateObjectForm(() => { + props.refreshFunction(); + }, { department: props.department_id }), + }) + : null, + ]); + + if (gatesSection) { + sections.push(gatesSection); + } + + const relaysSection = buildMenuSection("department-relays", t("admin.pos.settings_wheel.relays_section"), [ + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-relays-open-tab", { + icon: "fas fa-bolt", + label: t("admin.pos.settings_wheel.open_relays_tab"), + clickAction: () => openDepartmentWorkspaceTab("relays"), + }) + : null, + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-relays-open-legacy", { + icon: "fas fa-external-link-alt", + label: t("admin.pos.settings_wheel.open_legacy_relays"), + clickAction: () => SessionUser.functions.redirectTo.superUser("/department/relays", true), + }) + : null, + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-relays-create", { + icon: "fas fa-plus", + label: t("admin.pos.settings_wheel.add_relay"), + clickAction: () => + SessionUser.objects.department_relays.showCreateObjectForm(() => { + props.refreshFunction(); + }, { department: props.department_id }), + }) + : null, + ]); + + if (relaysSection) { + sections.push(relaysSection); + } + + const gatewaysSection = buildMenuSection( + "department-gateways", + t("admin.pos.settings_wheel.gateways_section"), + [ + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-gateways-open-tab", { + icon: "fas fa-network-wired", + label: t("admin.pos.settings_wheel.open_gateways_tab"), + clickAction: () => openDepartmentWorkspaceTab("gateways"), + }) + : null, + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-gateways-open-fleet", { + icon: "fas fa-sitemap", + label: t("admin.pos.settings_wheel.open_fleet_landing"), + clickAction: () => SessionUser.functions.redirectTo.superUser("/configuration/edgegateway", true), + }) + : null, + SessionUser.canAccessSuperUser() + ? buildMenuAction("department-gateways-open-primary", { + icon: "fas fa-star", + label: t("admin.pos.settings_wheel.open_primary_gateway"), + clickAction: () => openPrimaryGatewayPage(), + }) + : null, + ] + ); + + if (gatewaysSection) { + sections.push(gatewaysSection); + } + } + if (hasUser.value && SessionUser.canAccessSuperUser()) { - const customerSection = buildMenuSection("customer", SessionUser.objects.global.language.customer, [ - userIdFromCustomerNumber.value - ? buildMenuAction("customer-view", { + const customerSectionItems = userIdFromCustomerNumber.value + ? [ + buildMenuAction("customer-view", { icon: "fas fa-user-edit", label: t("admin.pos.settings_wheel.view_customer_new_tab"), clickAction: () => SessionUser.functions.redirectTo.superUser("/users/" + userIdFromCustomerNumber.value, true), - }) - : null, - userIdFromCustomerNumber.value - ? buildMenuAction("customer-login", { + }), + buildMenuAction("customer-login", { icon: "fas fa-user-shield", label: t("admin.pos.settings_wheel.login_as_user"), clickAction: () => SessionUser.superUser.intimidate.intimidateUser(userIdFromCustomerNumber.value), - }) - : null, - userIdFromCustomerNumber.value - ? buildMenuAction("customer-login-qr", { + }), + buildMenuAction("customer-login-qr", { icon: "fas fa-user-shield", label: t("admin.pos.settings_wheel.login_as_user_qr"), clickAction: () => onClickShowImpersonationQRCode(userIdFromCustomerNumber.value), - }) - : null, - buildMenuAction("customer-change-password", { - icon: "fas fa-key", - label: t("admin.pos.settings_wheel.change_password"), - clickAction: () => showSetCustomerPassword(userIdFromCustomerNumber.value), - }), - buildMenuAction("customer-show", { - icon: "fas fa-user", - label: t("admin.pos.settings_wheel.show_customer"), - clickAction: () => { - customerModalVisible.value = true; - }, - }), - ]); + }), + buildMenuAction("customer-change-password", { + icon: "fas fa-key", + label: t("admin.pos.settings_wheel.change_password"), + clickAction: () => showSetCustomerPassword(userIdFromCustomerNumber.value), + }), + buildMenuAction("customer-show", { + icon: "fas fa-user", + label: t("admin.pos.settings_wheel.show_customer"), + clickAction: () => { + customerModalVisible.value = true; + }, + }), + ] + : isResolvingUserId.value + ? [ + buildMenuAction("customer-loading", { + disabled: true, + icon: "fas fa-spinner", + label: t("global.loading"), + }), + ] + : []; + const customerSection = buildMenuSection( + "customer", + t("admin.pos.settings_wheel.customer_section"), + customerSectionItems + ); if (customerSection) { sections.push(customerSection); } + + if (canViewCustomerRules.value) { + const ruleSection = buildMenuSection( + "rules", + t("admin.pos.settings_wheel.rules_section"), + customerRuleAttributesLoading.value && !customerRuleAttributesLoaded.value + ? [ + buildMenuAction("customer-rules-loading", { + disabled: true, + icon: "fas fa-spinner", + label: t("global.loading"), + }), + ] + : customerRuleItems.value.map((rule) => + buildMenuToggleAction(rule.key, { + description: rule.description, + disabled: rule.disabled, + label: rule.label, + testId: rule.testId, + value: rule.value, + clickAction: () => toggleCustomerRuleAttribute(rule.attribute, !rule.value), + }) + ) + ); + + if (ruleSection) { + sections.push(ruleSection); + } + } + + const shortcutsSection = buildMenuSection( + "shortcuts", + t("admin.pos.settings_wheel.shortcuts_section"), + userIdFromCustomerNumber.value + ? customerShortcutItems.value + : isResolvingUserId.value + ? [ + buildMenuAction("customer-shortcuts-loading", { + disabled: true, + icon: "fas fa-spinner", + label: t("global.loading"), + }), + ] + : [] + ); + + if (shortcutsSection) { + sections.push(shortcutsSection); + } } if (props.reg_1 || props.reg_2) { @@ -1598,7 +1793,10 @@ const syncDesktopFlyoutState = (triggerRect) => { return; } - const canUseHover = window.matchMedia("(hover: hover) and (pointer: fine)").matches; + const canUseHover = + typeof window.matchMedia === "function" + ? window.matchMedia("(hover: hover) and (pointer: fine)").matches + : false; const hasSections = desktopFlyoutMenuSections.value.length > 0; const hasSpaceForFlyout = !!triggerRect && triggerRect.right - getDesktopFlyoutEstimatedWidth() >= desktopFlyoutPanelGapPx; @@ -1641,44 +1839,38 @@ const setActiveDesktopFlyoutSection = (sectionKey) => { - - - -