From fc26e12013004c57c821d2fede833605f5062aee Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Thu, 23 Apr 2026 10:32:08 +0200 Subject: [PATCH] Remove configuration cache problems report --- build/reports/problems/problems-report.html | 663 --------------- iuytu.txt | 0 .../buttons/ActionSettingsWheelButton.vue | 752 +++++++++++++++-- .../PosDepartmentStepMobile2Categories.vue | 131 ++- .../PosDepartmentStepMobile2Products.vue | 128 ++- ...osDepartmentStepMobilePopupAddCustomer.vue | 221 +---- .../objects/PosDepartmentStepMobileFlow.vue | 14 + .../superuser/tables/departmentGatesTable.vue | 11 + .../superuser/tables/departmentLanesTable.vue | 17 + .../tables/numberPlateScannersTable.vue | 13 +- .../department/pos/SelectProductsFormPOS.vue | 420 +++++++--- .../pos/input/customerSearchFieldPos.vue | 239 +++++- .../numberplatescanners/Scanners.vue | 10 +- .../SessionUser/Objects/DepartmentGates.vue | 22 +- src/components/shop/Products.vue | 63 ++ src/composables/usePosAddCustomerForm.js | 211 +++++ .../EdgeGatewayDepartmentWorkspace.vue | 789 ++++++++++++++++++ .../edgeGateways/EdgeGatewayFleetLanding.vue | 329 ++++++++ src/i18n/locales/da.json | 2 + src/i18n/locales/de.json | 2 + src/i18n/locales/en.json | 2 + src/i18n/locales/no.json | 2 + src/i18n/locales/sv.json | 2 + src/services/edgeGateways.js | 15 + .../superUserDashboard/DepartmentGates.vue | 40 + .../superUserDashboard/DepartmentLanes.vue | 38 +- .../DepartmentSelfServe.vue | 41 +- .../EdgeGatewaysWorkspacePage.vue | 3 + .../NumberPlateScanners.vue | 41 +- .../DepartmentGatewaysWorkspacePage.vue | 23 +- tests/e2e/admin-pos-orders.spec.ts | 37 +- tests/e2e/edge-gateways.routes.spec.js | 21 +- tests/e2e/edge-gateways.smoke.spec.js | 40 + tests/e2e/edge-gateways.visual.spec.js | 1 + tests/e2e/pos-flow.spec.js | 272 +++++- tests/e2e/pos-mobile-order-flow.spec.js | 45 + tests/e2e/support/bookingFlow.ts | 41 +- tests/e2e/support/mobilePos.js | 13 + tests/e2e/support/network.js | 687 ++++++++++++++- tests/e2e/userBookWash.spec.ts | 55 +- .../unit/action-settings-wheel-button.spec.js | 233 +++++- tests/unit/customer-search-field-pos.spec.js | 303 +++++++ tests/unit/edge-gateway-service.spec.js | 67 ++ tests/unit/edge-gateway-workspace.spec.js | 28 +- tests/unit/select-vehicle-form-pos.spec.js | 10 + tmp-acl-test.txt | 1 + tmp-codex-vite.pid | 1 + vite.config.js | 20 +- 48 files changed, 4994 insertions(+), 1125 deletions(-) delete mode 100644 build/reports/problems/problems-report.html create mode 100644 iuytu.txt create mode 100644 src/composables/usePosAddCustomerForm.js create mode 100644 src/features/edgeGateways/EdgeGatewayDepartmentWorkspace.vue create mode 100644 src/features/edgeGateways/EdgeGatewayFleetLanding.vue create mode 100644 tests/unit/customer-search-field-pos.spec.js create mode 100644 tmp-acl-test.txt create mode 100644 tmp-codex-vite.pid diff --git a/build/reports/problems/problems-report.html b/build/reports/problems/problems-report.html deleted file mode 100644 index fb781dd1..00000000 --- a/build/reports/problems/problems-report.html +++ /dev/null @@ -1,663 +0,0 @@ - - - - - - - - - - - - - Gradle Configuration Cache - - - -
- -
- Loading... -
- - - - - - diff --git a/iuytu.txt b/iuytu.txt new file mode 100644 index 00000000..e69de29b diff --git a/src/components/displays/buttons/ActionSettingsWheelButton.vue b/src/components/displays/buttons/ActionSettingsWheelButton.vue index 0b6cd3fc..54dc752f 100644 --- a/src/components/displays/buttons/ActionSettingsWheelButton.vue +++ b/src/components/displays/buttons/ActionSettingsWheelButton.vue @@ -11,6 +11,8 @@ import { useI18n } from "vue-i18n"; const { t } = useI18n(); const slots = useSlots(); +const imageExtensions = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".svg"]; +const officeExtensions = [".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx"]; const emit = defineEmits(["deleted"]); const emitDeleted = () => { emit("deleted"); @@ -94,6 +96,13 @@ const shouldOpenDropdownUp = ref(false); const dropdownMaxHeight = ref(null); const isDesktopFlyoutLayout = ref(false); const activeDesktopFlyoutSectionKey = ref(null); +const activeAttachmentId = ref(null); +const previewLoadingAttachmentId = ref(null); +const deletingAttachmentId = ref(null); +const previewSourcesById = ref({}); +const previewRequestsInFlight = new Set(); +const generatedObjectUrls = new Set(); +let previewStateGeneration = 0; const desktopFlyoutMinViewportWidth = 1400; const desktopFlyoutRootPanelWidthRem = 15; const desktopFlyoutSubmenuWidthRem = 17; @@ -316,6 +325,7 @@ watch(isDropdownOpen, async (isOpen) => { if (!isOpen) { isDesktopFlyoutLayout.value = false; activeDesktopFlyoutSectionKey.value = null; + clearAttachmentPreviewState(); resetDropdownLayout(); return; } @@ -391,6 +401,383 @@ const hasUser = computed(() => Boolean(props.user_id || props.customer_number || const attachmentsFromOrder = ref([]); const attachmentsFromOrderError = ref(null); + +const isObjectUrl = (value) => typeof value === "string" && value.startsWith("blob:"); + +const releaseObjectUrl = (value) => { + if (!isObjectUrl(value)) { + return; + } + + URL.revokeObjectURL(value); + generatedObjectUrls.delete(value); +}; + +const clearAttachmentPreviewState = () => { + previewStateGeneration += 1; + previewRequestsInFlight.clear(); + Object.values(previewSourcesById.value).forEach((value) => { + releaseObjectUrl(value); + }); + previewSourcesById.value = {}; + previewLoadingAttachmentId.value = null; + activeAttachmentId.value = null; +}; + +const getAttachmentLabel = (attachment) => { + return ( + attachment?.content?.document || + attachment?.content?.image || + attachment?.content?.other || + `Attachment ${attachment?.id ?? ""}`.trim() + ); +}; + +const getAttachmentExtension = (attachment) => { + const match = getAttachmentLabel(attachment) + .toLowerCase() + .match(/(\.[a-z0-9]+)$/); + + return match?.[1] || ""; +}; + +const getAttachmentPreviewKind = (attachment) => { + const extension = getAttachmentExtension(attachment); + + if (attachment?.content?.image || imageExtensions.includes(extension)) { + return "image"; + } + + if (attachment?.content?.document || extension === ".pdf") { + return officeExtensions.includes(extension) ? "office" : "document"; + } + + if (officeExtensions.includes(extension)) { + return "office"; + } + + if (String(attachment?.content?.other || "").startsWith("http")) { + return "link"; + } + + if (attachment?.content?.other) { + return "text"; + } + + return "none"; +}; + +const activeAttachment = computed(() => { + return attachmentsFromOrder.value.find((attachment) => attachment.id === activeAttachmentId.value) || null; +}); + +const activeAttachmentPreviewKind = computed(() => { + if (!activeAttachment.value) { + return "none"; + } + + return getAttachmentPreviewKind(activeAttachment.value); +}); + +const activeAttachmentPreviewSource = computed(() => { + if (!activeAttachment.value) { + return null; + } + + return previewSourcesById.value[activeAttachment.value.id] ?? null; +}); + +const hasCachedPreviewSource = (attachmentId) => { + return Object.prototype.hasOwnProperty.call(previewSourcesById.value, attachmentId); +}; + +const createEmbeddablePreviewUrl = async (downloadLink) => { + if (!downloadLink) { + return null; + } + + try { + const response = await fetch(downloadLink, { method: "GET" }); + if (!response.ok) { + return null; + } + + const fileBlob = await response.blob(); + if (!fileBlob || fileBlob.size === 0) { + return null; + } + + const objectUrl = URL.createObjectURL(fileBlob); + generatedObjectUrls.add(objectUrl); + return objectUrl; + } catch (error) { + console.warn("Unable to create embeddable attachment preview blob", error); + return null; + } +}; + +const loadOrderAttachments = async () => { + if (!props.order_id) { + attachmentsFromOrder.value = []; + attachmentsFromOrderError.value = null; + clearAttachmentPreviewState(); + return []; + } + + attachmentsFromOrderError.value = null; + + try { + let response = await SessionUser.objects.orders.functions.fetchAttachments(props.order_id); + if (!Array.isArray(response)) { + response = []; + } + + attachmentsFromOrder.value = response; + + if (!response.some((attachment) => attachment.id === activeAttachmentId.value)) { + activeAttachmentId.value = null; + } + + return response; + } catch (error) { + const parsedErrorMessage = SessionUser.functions.parseErrorMessage(error); + console.warn("Error fetching attachments from order:", error, parsedErrorMessage); + attachmentsFromOrder.value = []; + attachmentsFromOrderError.value = `ERROR: ${parsedErrorMessage || "Failed to fetch attachments"}`; + clearAttachmentPreviewState(); + return []; + } +}; + +const resolveAttachmentDownloadLink = async (attachment) => { + if (!props.order_id || !attachment?.id) { + return null; + } + + return SessionUser.objects.orders.functions.downloadAttachment(props.order_id, attachment.id, false); +}; + +const ensurePreviewSource = async (attachment) => { + if (!attachment) { + return null; + } + + const currentPreviewStateGeneration = previewStateGeneration; + const previewKind = getAttachmentPreviewKind(attachment); + if (!["image", "document"].includes(previewKind)) { + return null; + } + + if (hasCachedPreviewSource(attachment.id) || previewRequestsInFlight.has(attachment.id)) { + return previewSourcesById.value[attachment.id] ?? null; + } + + previewRequestsInFlight.add(attachment.id); + previewLoadingAttachmentId.value = attachment.id; + + try { + const downloadLink = await resolveAttachmentDownloadLink(attachment); + const previewSource = await createEmbeddablePreviewUrl(downloadLink); + + if (currentPreviewStateGeneration !== previewStateGeneration) { + releaseObjectUrl(previewSource); + return null; + } + + previewSourcesById.value = { + ...previewSourcesById.value, + [attachment.id]: previewSource, + }; + return previewSource; + } catch (error) { + console.warn("Unable to load attachment preview", attachment.id, error); + if (currentPreviewStateGeneration === previewStateGeneration) { + previewSourcesById.value = { + ...previewSourcesById.value, + [attachment.id]: null, + }; + } + return null; + } finally { + previewRequestsInFlight.delete(attachment.id); + if (previewLoadingAttachmentId.value === attachment.id) { + previewLoadingAttachmentId.value = null; + } + } +}; + +const setActiveAttachment = async (attachment) => { + if (!attachment) { + activeAttachmentId.value = null; + return; + } + + activeAttachmentId.value = attachment.id; + await ensurePreviewSource(attachment); +}; + +const openAttachmentInNewTab = (url) => { + if (!url) { + return null; + } + + return window.open(url, "_blank", "noopener,noreferrer"); +}; + +const downloadAttachmentFile = (url, attachment) => { + if (!url) { + return; + } + + const link = document.createElement("a"); + link.href = url; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.download = getAttachmentLabel(attachment); + document.body.appendChild(link); + link.click(); + link.remove(); +}; + +const previewAttachment = async (attachment) => { + try { + const previewKind = getAttachmentPreviewKind(attachment); + if (["image", "document"].includes(previewKind)) { + const previewSource = (await ensurePreviewSource(attachment)) || (await resolveAttachmentDownloadLink(attachment)); + openAttachmentInNewTab(previewSource); + return; + } + + const downloadLink = await resolveAttachmentDownloadLink(attachment); + openAttachmentInNewTab(downloadLink); + } catch (error) { + console.warn("Unable to preview attachment", attachment?.id, error); + Swal.fire({ + title: t("admin.pos.settings_wheel.error"), + text: t("admin.pos.settings_wheel.error_downloading_attachment"), + icon: "error", + }); + } +}; + +const downloadAttachment = async (attachment) => { + try { + const downloadLink = await resolveAttachmentDownloadLink(attachment); + downloadAttachmentFile(downloadLink, attachment); + } catch (error) { + Swal.fire({ + title: t("admin.pos.settings_wheel.error"), + text: t("admin.pos.settings_wheel.error_downloading_attachment"), + icon: "error", + }); + } +}; + +const printAttachment = async (attachment) => { + try { + const previewKind = getAttachmentPreviewKind(attachment); + const downloadLink = await resolveAttachmentDownloadLink(attachment); + + if (!downloadLink) { + return; + } + + if (!["image", "document"].includes(previewKind)) { + openAttachmentInNewTab(downloadLink); + return; + } + + const printableSource = (await ensurePreviewSource(attachment)) || downloadLink; + const printWindow = window.open("", "_blank", "noopener,noreferrer,width=960,height=720"); + + if (!printWindow) { + openAttachmentInNewTab(printableSource); + return; + } + + const escapedTitle = getAttachmentLabel(attachment).replace(/"/g, """); + const contentMarkup = + previewKind === "image" + ? `` + : ``; + + printWindow.document.write(` + + + ${escapedTitle} + + + ${contentMarkup} +`); + printWindow.document.close(); + + printWindow.addEventListener( + "load", + () => { + printWindow.focus(); + window.setTimeout(() => { + printWindow.print(); + }, 150); + }, + { once: true } + ); + } catch (error) { + console.warn("Unable to print attachment", attachment?.id, error); + Swal.fire({ + title: t("admin.pos.settings_wheel.error"), + text: t("admin.pos.settings_wheel.error_downloading_attachment"), + icon: "error", + }); + } +}; + +const removeAttachment = async (attachment) => { + if (!props.order_id || !attachment?.id || deletingAttachmentId.value === attachment.id) { + return; + } + + deletingAttachmentId.value = attachment.id; + + try { + const response = await SessionUser.objects.orders.functions.removeAttachment(props.order_id, attachment.id); + if (!response) { + throw new Error("Attachment delete failed"); + } + + releaseObjectUrl(previewSourcesById.value[attachment.id]); + + const nextPreviewSourcesById = { ...previewSourcesById.value }; + delete nextPreviewSourcesById[attachment.id]; + previewSourcesById.value = nextPreviewSourcesById; + + await loadOrderAttachments(); + await props.refreshFunction(); + } catch (error) { + console.warn("Unable to delete attachment", attachment?.id, error); + Swal.fire({ + title: t("admin.pos.settings_wheel.error"), + text: t("admin.pos.settings_wheel.error_downloading_attachment"), + icon: "error", + }); + } finally { + deletingAttachmentId.value = null; + } +}; + watch( () => [attachmentsFromOrder.value.length, attachmentsFromOrderError.value, userIdFromCustomerNumber.value], () => { @@ -400,47 +787,19 @@ watch( } ); +watch( + () => props.order_id, + () => { + void loadOrderAttachments(); + }, + { immediate: true } +); + onMounted(() => { document.addEventListener("click", onDocumentClick); document.addEventListener("keydown", onDocumentKeydown); window.addEventListener("resize", onViewportChange); document.addEventListener("scroll", onViewportChange, true); - - if (props.order_id) { - SessionUser.objects.orders.functions - .fetchAttachments(props.order_id) - .then((response) => { - /** - * [ - * { - * "id": 4396, - * "object_type": "`orders`", - * "object_id": 30242, - * "content": { - * "image": null, - * "document": "pdf_6912ea1886f15.pdf", - * "relation": null, - * "other": "wash_certificate" - * }, - * "created_at": "2025-11-11 08:47:36", - * "updated_at": null, - * "deleted_at": null - * } - * ] - */ - // Validate the response is an array - if (!Array.isArray(response)) { - response = []; - } - attachmentsFromOrder.value = response; - }) - .catch((error) => { - const parsedErrorMessage = SessionUser.functions.parseErrorMessage(error); - console.warn("Error fetching attachments from order:", error, parsedErrorMessage); - // Check if there's an error message in the response - attachmentsFromOrderError.value = `ERROR: ${parsedErrorMessage || "Failed to fetch attachments"}`; - }); - } }); onBeforeUnmount(() => { @@ -448,6 +807,7 @@ onBeforeUnmount(() => { document.removeEventListener("keydown", onDocumentKeydown); window.removeEventListener("resize", onViewportChange); document.removeEventListener("scroll", onViewportChange, true); + clearAttachmentPreviewState(); }); const onShowImpersonationQRCode = (src, directLink) => { @@ -838,8 +1198,8 @@ const buildMenuSection = (key, label, items) => { }; }; -const downloadOrderAttachment = (attachmentId) => - SessionUser.objects.orders.functions.downloadAttachment(props.order_id, attachmentId, true).catch(() => { +const downloadOrderAttachment = (attachment) => + downloadAttachment(attachment).catch(() => { Swal.fire({ title: t("admin.pos.settings_wheel.error"), text: t("admin.pos.settings_wheel.error_downloading_attachment"), @@ -1119,21 +1479,24 @@ const flatBuiltInMenuSections = computed(() => { if (vehicleSection) { sections.push(vehicleSection); } + } - if (attachmentsFromOrder.value.length > 0) { - const attachmentsSection = buildMenuSection("attachments", t("admin.pos.settings_wheel.attached_files"), [ - ...attachmentsFromOrder.value.map((attachment) => - buildMenuAction(`attachment-${attachment.id}`, { - icon: "fas fa-paperclip", - label: `${attachment.content?.document || attachment.content?.other}`, - clickAction: () => downloadOrderAttachment(attachment.id), - }) - ), - ]); + if (attachmentsFromOrder.value.length > 0) { + const attachmentsSection = buildMenuSection("attachments", t("admin.pos.settings_wheel.attached_files"), [ + ...attachmentsFromOrder.value.map((attachment) => + buildMenuAction(`attachment-${attachment.id}`, { + icon: "fas fa-paperclip", + label: getAttachmentLabel(attachment), + clickAction: () => downloadOrderAttachment(attachment), + }) + ), + ]); - if (attachmentsSection) { - sections.push(attachmentsSection); - } + if (attachmentsSection) { + sections.push({ + ...attachmentsSection, + attachments: [...attachmentsFromOrder.value], + }); } } @@ -1260,6 +1623,10 @@ const syncDesktopFlyoutState = (triggerRect) => { const setActiveDesktopFlyoutSection = (sectionKey) => { activeDesktopFlyoutSectionKey.value = sectionKey; + + if (sectionKey !== "attachments") { + activeAttachmentId.value = null; + } }; @@ -1341,17 +1708,44 @@ const setActiveDesktopFlyoutSection = (sectionKey) => {