diff --git a/src/assets/main.css b/src/assets/main.css
index 2d85b217..7ec26177 100644
--- a/src/assets/main.css
+++ b/src/assets/main.css
@@ -49,8 +49,26 @@ textarea.has-sharp-edges {
--bulma-card-shadow: none !important;
--bulma-skeleton-background: hsla(197 100% 35% / 0.70) !important;
+ --pleno-compact-table-header-font-size: 0.72rem;
+ --pleno-compact-table-header-line-height: 1;
}
+body:not(.pleno-large-table-headers) .table thead th {
+ font-size: var(--pleno-compact-table-header-font-size);
+ line-height: var(--pleno-compact-table-header-line-height);
+ white-space: nowrap;
+ vertical-align: middle;
+}
+
+.pleno-table-header-content {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.15rem;
+ max-width: 100%;
+ vertical-align: middle;
+}
+
+
/*:root {*/
/* --bulma-primary-h: 207deg !important;*/
/* --bulma-primary-s: 60% !important;*/
@@ -125,4 +143,4 @@ textarea.has-sharp-edges {
.mt-30-tablet {
margin-top: 30px !important;
}
-}
\ No newline at end of file
+}
diff --git a/src/components/displays/department/pos/orders/InvoiceCollectionSelectionActionWheel.vue b/src/components/displays/department/pos/orders/InvoiceCollectionSelectionActionWheel.vue
new file mode 100644
index 00000000..2122e683
--- /dev/null
+++ b/src/components/displays/department/pos/orders/InvoiceCollectionSelectionActionWheel.vue
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/displays/department/pos/orders/invoiceCollectionBulkActions.js b/src/components/displays/department/pos/orders/invoiceCollectionBulkActions.js
new file mode 100644
index 00000000..90556f40
--- /dev/null
+++ b/src/components/displays/department/pos/orders/invoiceCollectionBulkActions.js
@@ -0,0 +1,7 @@
+export const INVOICE_COLLECTION_BULK_ACTIONS = Object.freeze({
+ CLEAN_CUSTOMER_RULES: "remove_customer_rule_violations",
+ MERGE: "merge_collections",
+ SPLIT_BY_MONTH: "split_by_month",
+ RESET_HIDDEN_PRICES: "reset_hidden_item_prices",
+ QUEUE_ECONOMIC: "queue_economic",
+});
diff --git a/src/components/displays/department/pos/orders/ordersTable.vue b/src/components/displays/department/pos/orders/ordersTable.vue
index c9643db2..244c729e 100644
--- a/src/components/displays/department/pos/orders/ordersTable.vue
+++ b/src/components/displays/department/pos/orders/ordersTable.vue
@@ -1,12 +1,17 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/components/displays/superuser/tables/collectedOrderInvoicesTable.vue b/src/components/displays/superuser/tables/collectedOrderInvoicesTable.vue
index 416f62f3..c1c6544c 100644
--- a/src/components/displays/superuser/tables/collectedOrderInvoicesTable.vue
+++ b/src/components/displays/superuser/tables/collectedOrderInvoicesTable.vue
@@ -1,11 +1,10 @@
@@ -124,22 +130,28 @@ const onSelect = (id) => {
{{ SessionUser.functions.currency.toLocal(object.total_net_amount) }} |
-
-
-
-
-
-
+ |
+
+
+
+
+
+
+
|
-
-
+ |
+ onSelectorChecked(object.id, checked)"
+ />
|
@@ -159,4 +171,4 @@ const onSelect = (id) => {
\ No newline at end of file
+
diff --git a/src/i18n/generated/da-v2.json b/src/i18n/generated/da-v2.json
index 47135120..e085ae5f 100644
--- a/src/i18n/generated/da-v2.json
+++ b/src/i18n/generated/da-v2.json
@@ -4152,6 +4152,15 @@
"merge_requires_multiple_text": "Vælg mindst to fakturasamlinger for at sammenlægge dem.",
"merge_requires_multiple_title": "Vælg flere fakturasamlinger",
"merge_target_title": "Vælg målfakturasamling",
+ "menu": {
+ "collapse_all": "Klap alle sammen",
+ "expand_all": "Udvid alle",
+ "label": "Fakturasamling handlinger ({count})",
+ "modification_section": "Rediger valgte",
+ "select_all": "Vælg alle",
+ "selection_section": "Valg",
+ "unselect_all": "Fravælg alle"
+ },
"preview": {
"affected_examples": "Eksempler på ændringer",
"blocked_title": "Handlingen er blokeret",
diff --git a/src/i18n/generated/de-v2.json b/src/i18n/generated/de-v2.json
index ad93c54a..76290810 100644
--- a/src/i18n/generated/de-v2.json
+++ b/src/i18n/generated/de-v2.json
@@ -4263,6 +4263,15 @@
"merge_requires_multiple_text": "Wählen Sie mindestens zwei Rechnungssammlungen aus, um sie zusammenzuführen.",
"merge_requires_multiple_title": "Mehrere Rechnungssammlungen auswählen",
"merge_target_title": "Ziel-Rechnungssammlung auswählen",
+ "menu": {
+ "collapse_all": "Alle einklappen",
+ "expand_all": "Alle erweitern",
+ "label": "Rechnungssammlungsaktionen ({count})",
+ "modification_section": "Ausgewählte ändern",
+ "select_all": "Alle auswählen",
+ "selection_section": "Auswahl",
+ "unselect_all": "Alle abwählen"
+ },
"preview": {
"affected_examples": "Beispieländerungen",
"blocked_title": "Aktion ist blockiert",
diff --git a/src/i18n/generated/en-v2.json b/src/i18n/generated/en-v2.json
index 51e7e3c1..76212def 100644
--- a/src/i18n/generated/en-v2.json
+++ b/src/i18n/generated/en-v2.json
@@ -3984,6 +3984,15 @@
"merge_requires_multiple_text": "Select at least two invoice collections to merge them.",
"merge_requires_multiple_title": "Select multiple invoice collections",
"merge_target_title": "Select target invoice collection",
+ "menu": {
+ "collapse_all": "Collapse all",
+ "expand_all": "Expand all",
+ "label": "Invoice collection actions ({count})",
+ "modification_section": "Modify selected",
+ "select_all": "Select all",
+ "selection_section": "Selection",
+ "unselect_all": "Unselect all"
+ },
"preview": {
"affected_examples": "Example changes",
"blocked_title": "Action is blocked",
diff --git a/src/i18n/generated/global-v2.json b/src/i18n/generated/global-v2.json
index 20acd10f..df92bcf8 100644
--- a/src/i18n/generated/global-v2.json
+++ b/src/i18n/generated/global-v2.json
@@ -3237,6 +3237,15 @@
"merge_requires_multiple_text": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.merge_requires_multiple_text'}",
"merge_requires_multiple_title": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.merge_requires_multiple_title'}",
"merge_target_title": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.merge_target_title'}",
+ "menu": {
+ "collapse_all": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.collapse_all'}",
+ "expand_all": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.expand_all'}",
+ "label": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.label'}",
+ "modification_section": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.modification_section'}",
+ "select_all": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.select_all'}",
+ "selection_section": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.selection_section'}",
+ "unselect_all": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.unselect_all'}"
+ },
"preview": {
"affected_examples": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.affected_examples'}",
"blocked_title": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.blocked_title'}",
diff --git a/src/i18n/generated/no-v2.json b/src/i18n/generated/no-v2.json
index c56ad6e7..be4c736a 100644
--- a/src/i18n/generated/no-v2.json
+++ b/src/i18n/generated/no-v2.json
@@ -4266,6 +4266,15 @@
"merge_requires_multiple_text": "Velg minst to fakturasamlinger for å slå dem sammen.",
"merge_requires_multiple_title": "Velg flere fakturasamlinger",
"merge_target_title": "Velg målfakturasamling",
+ "menu": {
+ "collapse_all": "Slå sammen alle",
+ "expand_all": "Utvid alle",
+ "label": "Fakturasamling handlinger ({count})",
+ "modification_section": "Endre valgte",
+ "select_all": "Velg alle",
+ "selection_section": "Valg",
+ "unselect_all": "Fjern valg av alle"
+ },
"preview": {
"affected_examples": "Eksempler på endringer",
"blocked_title": "Handlingen er blokkert",
diff --git a/src/i18n/generated/sv-v2.json b/src/i18n/generated/sv-v2.json
index 6e37879b..35b81b53 100644
--- a/src/i18n/generated/sv-v2.json
+++ b/src/i18n/generated/sv-v2.json
@@ -4316,6 +4316,15 @@
"merge_requires_multiple_text": "Välj minst två fakturasamlingar för att slå ihop dem.",
"merge_requires_multiple_title": "Välj flera fakturasamlingar",
"merge_target_title": "Välj målfakturasamling",
+ "menu": {
+ "collapse_all": "Fäll ihop alla",
+ "expand_all": "Expandera alla",
+ "label": "Fakturasamlingsåtgärder ({count})",
+ "modification_section": "Ändra valda",
+ "select_all": "Välj alla",
+ "selection_section": "Val",
+ "unselect_all": "Avmarkera alla"
+ },
"preview": {
"affected_examples": "Exempel på ändringar",
"blocked_title": "Åtgärden är blockerad",
diff --git a/src/i18n/source/da/phrases/compat/invoicing_period/invoice_collection_actions.json b/src/i18n/source/da/phrases/compat/invoicing_period/invoice_collection_actions.json
index 159783ad..f7791772 100644
--- a/src/i18n/source/da/phrases/compat/invoicing_period/invoice_collection_actions.json
+++ b/src/i18n/source/da/phrases/compat/invoicing_period/invoice_collection_actions.json
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Vælg mindst to fakturasamlinger for at sammenlægge dem.",
"merge_requires_multiple_title": "Vælg flere fakturasamlinger",
"merge_target_title": "Vælg målfakturasamling",
+ "menu": {
+ "collapse_all": "Klap alle sammen",
+ "expand_all": "Udvid alle",
+ "label": "Fakturasamling handlinger ({count})",
+ "modification_section": "Rediger valgte",
+ "select_all": "Vælg alle",
+ "selection_section": "Valg",
+ "unselect_all": "Fravælg alle"
+ },
"preview": {
"affected_examples": "Eksempler på ændringer",
"blocked_title": "Handlingen er blokeret",
diff --git a/src/i18n/source/de/phrases/compat/invoicing_period/invoice_collection_actions.json b/src/i18n/source/de/phrases/compat/invoicing_period/invoice_collection_actions.json
index bc81c6ab..a805b2a9 100644
--- a/src/i18n/source/de/phrases/compat/invoicing_period/invoice_collection_actions.json
+++ b/src/i18n/source/de/phrases/compat/invoicing_period/invoice_collection_actions.json
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Wählen Sie mindestens zwei Rechnungssammlungen aus, um sie zusammenzuführen.",
"merge_requires_multiple_title": "Mehrere Rechnungssammlungen auswählen",
"merge_target_title": "Ziel-Rechnungssammlung auswählen",
+ "menu": {
+ "collapse_all": "Alle einklappen",
+ "expand_all": "Alle erweitern",
+ "label": "Rechnungssammlungsaktionen ({count})",
+ "modification_section": "Ausgewählte ändern",
+ "select_all": "Alle auswählen",
+ "selection_section": "Auswahl",
+ "unselect_all": "Alle abwählen"
+ },
"preview": {
"affected_examples": "Beispieländerungen",
"blocked_title": "Aktion ist blockiert",
diff --git a/src/i18n/source/en/phrases/compat/invoicing_period/invoice_collection_actions.json b/src/i18n/source/en/phrases/compat/invoicing_period/invoice_collection_actions.json
index 8ca0585f..2f804a85 100644
--- a/src/i18n/source/en/phrases/compat/invoicing_period/invoice_collection_actions.json
+++ b/src/i18n/source/en/phrases/compat/invoicing_period/invoice_collection_actions.json
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Select at least two invoice collections to merge them.",
"merge_requires_multiple_title": "Select multiple invoice collections",
"merge_target_title": "Select target invoice collection",
+ "menu": {
+ "collapse_all": "Collapse all",
+ "expand_all": "Expand all",
+ "label": "Invoice collection actions ({count})",
+ "modification_section": "Modify selected",
+ "select_all": "Select all",
+ "selection_section": "Selection",
+ "unselect_all": "Unselect all"
+ },
"preview": {
"affected_examples": "Example changes",
"blocked_title": "Action is blocked",
diff --git a/src/i18n/source/global/shared/invoicing_period/invoice_collection_actions.json b/src/i18n/source/global/shared/invoicing_period/invoice_collection_actions.json
index 6b69318b..1e209525 100644
--- a/src/i18n/source/global/shared/invoicing_period/invoice_collection_actions.json
+++ b/src/i18n/source/global/shared/invoicing_period/invoice_collection_actions.json
@@ -12,6 +12,15 @@
"merge_requires_multiple_text": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.merge_requires_multiple_text'}",
"merge_requires_multiple_title": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.merge_requires_multiple_title'}",
"merge_target_title": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.merge_target_title'}",
+ "menu": {
+ "collapse_all": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.collapse_all'}",
+ "expand_all": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.expand_all'}",
+ "label": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.label'}",
+ "modification_section": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.modification_section'}",
+ "select_all": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.select_all'}",
+ "selection_section": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.selection_section'}",
+ "unselect_all": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.unselect_all'}"
+ },
"preview": {
"affected_examples": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.affected_examples'}",
"blocked_title": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.blocked_title'}",
diff --git a/src/i18n/source/no/phrases/compat/invoicing_period/invoice_collection_actions.json b/src/i18n/source/no/phrases/compat/invoicing_period/invoice_collection_actions.json
index 2819e757..7aed0a47 100644
--- a/src/i18n/source/no/phrases/compat/invoicing_period/invoice_collection_actions.json
+++ b/src/i18n/source/no/phrases/compat/invoicing_period/invoice_collection_actions.json
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Velg minst to fakturasamlinger for å slå dem sammen.",
"merge_requires_multiple_title": "Velg flere fakturasamlinger",
"merge_target_title": "Velg målfakturasamling",
+ "menu": {
+ "collapse_all": "Slå sammen alle",
+ "expand_all": "Utvid alle",
+ "label": "Fakturasamling handlinger ({count})",
+ "modification_section": "Endre valgte",
+ "select_all": "Velg alle",
+ "selection_section": "Valg",
+ "unselect_all": "Fjern valg av alle"
+ },
"preview": {
"affected_examples": "Eksempler på endringer",
"blocked_title": "Handlingen er blokkert",
diff --git a/src/i18n/source/sv/phrases/compat/invoicing_period/invoice_collection_actions.json b/src/i18n/source/sv/phrases/compat/invoicing_period/invoice_collection_actions.json
index 294bc1f9..af91628f 100644
--- a/src/i18n/source/sv/phrases/compat/invoicing_period/invoice_collection_actions.json
+++ b/src/i18n/source/sv/phrases/compat/invoicing_period/invoice_collection_actions.json
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Välj minst två fakturasamlingar för att slå ihop dem.",
"merge_requires_multiple_title": "Välj flera fakturasamlingar",
"merge_target_title": "Välj målfakturasamling",
+ "menu": {
+ "collapse_all": "Fäll ihop alla",
+ "expand_all": "Expandera alla",
+ "label": "Fakturasamlingsåtgärder ({count})",
+ "modification_section": "Ändra valda",
+ "select_all": "Välj alla",
+ "selection_section": "Val",
+ "unselect_all": "Avmarkera alla"
+ },
"preview": {
"affected_examples": "Exempel på ändringar",
"blocked_title": "Åtgärden är blockerad",
diff --git a/src/main.js b/src/main.js
index b335ccda..525ac2b8 100644
--- a/src/main.js
+++ b/src/main.js
@@ -23,6 +23,7 @@ import {
installReleaseErrorInstrumentation,
} from '@/services/releaseTimeline.js';
import { RELEASE_RUNTIME_GLOBAL_KEY } from '@/services/releaseBootstrap.js';
+import '@/services/tableHeaderPreferences.js';
import { IS_DEV } from './config';
const VITE_BUILD_DATE = import.meta.env.VITE_BUILD_DATE || '';
diff --git a/src/services/tableHeaderPreferences.js b/src/services/tableHeaderPreferences.js
new file mode 100644
index 00000000..192ac16a
--- /dev/null
+++ b/src/services/tableHeaderPreferences.js
@@ -0,0 +1,52 @@
+import { ref } from "vue";
+
+export const LARGE_TABLE_HEADERS_STORAGE_KEY = "pleno.useLargeTableHeaders";
+export const LARGE_TABLE_HEADERS_BODY_CLASS = "pleno-large-table-headers";
+
+const applyLargeTableHeadersClass = (value) => {
+ if (typeof document === "undefined" || !document.body) {
+ return;
+ }
+
+ document.body.classList.toggle(LARGE_TABLE_HEADERS_BODY_CLASS, Boolean(value));
+};
+
+const readStoredBoolean = () => {
+ if (typeof window === "undefined" || !window.localStorage) {
+ return false;
+ }
+
+ try {
+ return window.localStorage.getItem(LARGE_TABLE_HEADERS_STORAGE_KEY) === "true";
+ } catch {
+ return false;
+ }
+};
+
+export const useLargeTableHeaders = ref(readStoredBoolean());
+applyLargeTableHeadersClass(useLargeTableHeaders.value);
+
+export const setUseLargeTableHeaders = (value) => {
+ const nextValue = Boolean(value);
+ useLargeTableHeaders.value = nextValue;
+ applyLargeTableHeadersClass(nextValue);
+
+ if (typeof window === "undefined" || !window.localStorage) {
+ return;
+ }
+
+ try {
+ window.localStorage.setItem(LARGE_TABLE_HEADERS_STORAGE_KEY, nextValue ? "true" : "false");
+ } catch {
+ // Storage is optional; keep the in-memory preference for this session.
+ }
+};
+
+if (typeof window !== "undefined") {
+ window.addEventListener("storage", (event) => {
+ if (event.key === LARGE_TABLE_HEADERS_STORAGE_KEY) {
+ useLargeTableHeaders.value = event.newValue === "true";
+ applyLargeTableHeadersClass(useLargeTableHeaders.value);
+ }
+ });
+}
diff --git a/src/views/dashboards/userDashboard/profile/UserProfile.vue b/src/views/dashboards/userDashboard/profile/UserProfile.vue
index ec478c38..ab3d132d 100644
--- a/src/views/dashboards/userDashboard/profile/UserProfile.vue
+++ b/src/views/dashboards/userDashboard/profile/UserProfile.vue
@@ -15,6 +15,7 @@ import ShowErrorField from "@/components/global/ShowErrorField.vue";
import SubuserGrantSelector from "@/components/session/subuser/SubuserGrantSelector.vue";
import { parseError, removeError, addError } from "@/components/request/HandleGlobalError.vue";
import ConfigurationSwitch from "@/components/displays/superuser/configuration/ConfigurationSwitch.vue";
+import { setUseLargeTableHeaders, useLargeTableHeaders } from "@/services/tableHeaderPreferences.js";
const { t } = useI18n();
const isLoading = ref(false);
@@ -792,6 +793,22 @@ const onClickSaveWashCertificateEmail = async () => {
{{ $t("user_dashboard.profile.driver.customer_switch_help") }}
+
+
+
+
{
await primeOperatorSession(page, "pos-orders-action-menu-layout-token", SUPERUSER_POS_PERMISSIONS);
});
+ test("keeps desktop order list row cells aligned", async ({ page }) => {
+ await page.goto("/admin/12/modules/pos/orders");
+ await expect(page.locator("table")).toBeVisible();
+
+ const row = getVisibleTestId(page, "pos-order-list-settings-54518").locator("xpath=ancestor::tr").first();
+ await expect(row).toBeVisible();
+
+ const rowMetrics = await row.evaluate((rowElement) => {
+ const cells = Array.from(rowElement.children).filter((child) => child.tagName.toLowerCase() === "td");
+ const idCell = rowElement.querySelector(".pos-orders-table-id-cell");
+ const idIndicator = idCell?.querySelector(".pos-orders-table-id-cell-content");
+ const cellSummaries = cells.map((cell) => {
+ const rect = cell.getBoundingClientRect();
+ return {
+ className: cell.className,
+ verticalAlign: window.getComputedStyle(cell).verticalAlign,
+ width: rect.width,
+ };
+ });
+ const idCellRect = idCell?.getBoundingClientRect();
+ const idIndicatorRect = idIndicator?.getBoundingClientRect();
+
+ return {
+ cellSummaries,
+ idCenterDelta:
+ idCellRect && idIndicatorRect
+ ? Math.abs(idIndicatorRect.left + idIndicatorRect.width / 2 - (idCellRect.left + idCellRect.width / 2))
+ : null,
+ idCellWidth: idCellRect?.width ?? null,
+ };
+ });
+
+ expect(rowMetrics.cellSummaries.length).toBeGreaterThan(5);
+ expect(rowMetrics.cellSummaries.every((cell) => cell.verticalAlign === "middle")).toBe(true);
+ expect(rowMetrics.idCenterDelta).not.toBeNull();
+ expect(rowMetrics.idCenterDelta ?? 999).toBeLessThanOrEqual(2);
+ expect(rowMetrics.idCellWidth ?? 999).toBeLessThanOrEqual(100);
+ expect(
+ rowMetrics.cellSummaries.some((cell) => String(cell.className).includes("pos-orders-table-actions-cell"))
+ ).toBe(true);
+ });
+
test("keeps tall menus within the usable viewport and starts from the first action", async ({ page }) => {
await page.goto("/admin/12/modules/pos/orders");
await expect(page.locator("table")).toBeVisible();
diff --git a/tests/e2e/change-invoice-collection.spec.ts b/tests/e2e/change-invoice-collection.spec.ts
index 8f779550..f0de8832 100644
--- a/tests/e2e/change-invoice-collection.spec.ts
+++ b/tests/e2e/change-invoice-collection.spec.ts
@@ -166,11 +166,11 @@ test.describe("POS order actions", () => {
"Expected invoice collection picker modal to open after clicking the action."
).toBeVisible();
- const selectButtons = pickerModal.getByRole("button", { name: /Vælg|Select/i });
- const selectableCount = await selectButtons.count();
+ const selectCheckboxes = pickerModal.locator('[data-testid^="collected-invoice-selector-"] input[type="checkbox"]');
+ const selectableCount = await selectCheckboxes.count();
test.skip(selectableCount === 0, "No invoice collections available for selection in test data.");
- await selectButtons.first().click();
+ await selectCheckboxes.first().check();
await expect(page).toHaveURL(beforeUrl);
await expect(page.locator(".swal2-container, .swal2-popup")).toContainText(
diff --git a/tests/unit/collected-order-invoices-table.spec.js b/tests/unit/collected-order-invoices-table.spec.js
new file mode 100644
index 00000000..2f73c055
--- /dev/null
+++ b/tests/unit/collected-order-invoices-table.spec.js
@@ -0,0 +1,186 @@
+// @vitest-environment jsdom
+import { mount } from "@vue/test-utils";
+import { describe, expect, it, vi } from "vitest";
+
+import CollectedOrderInvoicesTable from "@/components/displays/superuser/tables/collectedOrderInvoicesTable.vue";
+import CollectedOrderInvoicesDefaultTable from "@/components/displays/superuser/tables/CollectedOrderInvoicesDefaultTable.vue";
+
+vi.mock("@/components/pagination/paginatedList.vue", () => ({
+ usePaginatedListInstance: () => ({
+ loadList: vi.fn(),
+ }),
+}));
+
+vi.mock("@/components/session/token/SessionUser.vue", () => ({
+ SessionUser: {
+ objects: {
+ collectedOrderInvoices: {
+ columns: {
+ id: { label: "ID" },
+ customer_number: { label: "Customer number" },
+ processor: { label: "Processor" },
+ created_at: { label: "Created" },
+ name: { label: "Name" },
+ notes: { label: "Notes" },
+ external_id: { label: "External ID" },
+ po_number: { label: "PO" },
+ closed_at: { label: "Closed" },
+ updated_at: { label: "Updated" },
+ },
+ meta: {
+ labels: {
+ multiple: "invoice collections",
+ },
+ },
+ },
+ global: {
+ language: {
+ customer_name: "Customer",
+ },
+ },
+ orders: {
+ meta: {
+ title: "Orders",
+ },
+ },
+ },
+ functions: {
+ currency: {
+ toLocal: (value) => String(value ?? ""),
+ },
+ },
+ },
+}));
+
+vi.mock("vue-i18n", () => ({
+ useI18n: () => ({
+ t: (key) => key,
+ }),
+ createI18n: () => ({
+ global: {
+ t: (key) => key,
+ locale: {
+ value: "en",
+ },
+ },
+ }),
+}));
+
+const EditableTableColumnStub = {
+ props: ["object", "column", "parseFunction"],
+ template: "{{ parseFunction ? parseFunction(object[column]) : object[column] }} | ",
+};
+
+const ActionSettingsWheelButtonStub = {
+ template: '
',
+};
+
+const ActionSettingsWheelItemStub = {
+ template: '',
+};
+
+const BCheckboxStub = {
+ props: ["modelValue"],
+ emits: ["update:modelValue"],
+ template: `
+
+ `,
+};
+
+const mountSelectorTable = (props = {}) =>
+ mount(CollectedOrderInvoicesTable, {
+ props: {
+ objects: [
+ {
+ id: 42,
+ objects: 1,
+ customer_name: "Acme",
+ customer_number: 1001,
+ processor: 1,
+ orders: 2,
+ closed_at: null,
+ created_at: "2026-07-01",
+ total_net_amount: 123,
+ },
+ ],
+ showEmpty: true,
+ ...props,
+ },
+ global: {
+ mocks: {
+ $t: (key) => key,
+ },
+ stubs: {
+ EditableTableColumn: EditableTableColumnStub,
+ ActionSettingsWheelButton: ActionSettingsWheelButtonStub,
+ ActionSettingsWheelItem: ActionSettingsWheelItemStub,
+ BCheckbox: BCheckboxStub,
+ },
+ },
+ });
+
+describe("CollectedOrderInvoicesTable", () => {
+ it("uses a Buefy checkbox for selector mode", async () => {
+ const onSelected = vi.fn();
+ const wrapper = mountSelectorTable({
+ isSelector: true,
+ onSelected,
+ });
+
+ expect(wrapper.find("button.button.is-small.is-dark").exists()).toBe(false);
+
+ await wrapper.get('[data-testid="collected-invoice-selector-42"] input').setValue(true);
+
+ expect(onSelected).toHaveBeenCalledWith(42);
+ expect(wrapper.emitted("update:selected")?.[0]).toEqual([42]);
+ });
+
+ it("keeps a hidden attachment-width buffer before collected invoice row actions", () => {
+ const wrapper = mountSelectorTable();
+
+ expect(wrapper.get('[data-testid="superuser-invoice-action-buffer"]').exists()).toBe(true);
+ expect(wrapper.get('[data-testid="action-settings-wheel-stub"]').exists()).toBe(true);
+ });
+});
+
+describe("CollectedOrderInvoicesDefaultTable", () => {
+ it("keeps a hidden attachment-width buffer before default invoice row actions", () => {
+ const wrapper = mount(CollectedOrderInvoicesDefaultTable, {
+ props: {
+ objects: [
+ {
+ id: 43,
+ customer_number: 1002,
+ name: "Acme July",
+ notes: null,
+ processor: 1,
+ external_id: null,
+ po_number: null,
+ closed_at: null,
+ created_at: "2026-07-01",
+ updated_at: "2026-07-02",
+ },
+ ],
+ },
+ global: {
+ mocks: {
+ $t: (key) => key,
+ },
+ stubs: {
+ ActionSettingsWheelButton: ActionSettingsWheelButtonStub,
+ ActionSettingsWheelItem: ActionSettingsWheelItemStub,
+ },
+ },
+ });
+
+ expect(wrapper.get('[data-testid="superuser-invoice-action-buffer"]').exists()).toBe(true);
+ expect(wrapper.get('[data-testid="action-settings-wheel-stub"]').exists()).toBe(true);
+ });
+});
diff --git a/tests/unit/invoice-collection-selection-action-wheel.spec.js b/tests/unit/invoice-collection-selection-action-wheel.spec.js
new file mode 100644
index 00000000..62a7357b
--- /dev/null
+++ b/tests/unit/invoice-collection-selection-action-wheel.spec.js
@@ -0,0 +1,187 @@
+// @vitest-environment jsdom
+import { defineComponent } from "vue";
+import { mount } from "@vue/test-utils";
+import { describe, expect, it, vi } from "vitest";
+import InvoiceCollectionSelectionActionWheel from "@/components/displays/department/pos/orders/InvoiceCollectionSelectionActionWheel.vue";
+import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
+
+vi.mock("vue-i18n", () => ({
+ useI18n: () => ({
+ t: (key, params = {}) => (params.count === undefined ? key : `${key}:${params.count}`),
+ }),
+ createI18n: () => ({
+ global: {
+ t: (key) => key,
+ },
+ install: () => {},
+ }),
+}));
+
+vi.mock("@/components/displays/buttons/ActionSettingsWheelButton.vue", () => ({
+ default: {
+ template: '
',
+ },
+}));
+
+vi.mock("@/components/displays/buttons/ActionSettingsWheelItemLabel.vue", () => ({
+ default: {
+ props: ["label"],
+ template: '{{ label }}
',
+ },
+}));
+
+vi.mock("@/components/displays/buttons/ActionSettingsWheelItem.vue", () => ({
+ default: {
+ props: {
+ clickAction: {
+ type: Function,
+ default: null,
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ label: {
+ type: String,
+ default: "",
+ },
+ testId: {
+ type: String,
+ default: "",
+ },
+ },
+ template: `
+
+ `,
+ },
+}));
+
+const ActionSettingsWheelButtonStub = {
+ template: '
',
+};
+
+const ActionSettingsWheelItemLabelStub = {
+ props: ["label"],
+ template: '{{ label }}
',
+};
+
+const ActionSettingsWheelItemStub = defineComponent({
+ name: "ActionSettingsWheelItem",
+ props: {
+ clickAction: {
+ type: Function,
+ default: null,
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ label: {
+ type: String,
+ default: "",
+ },
+ testId: {
+ type: String,
+ default: "",
+ },
+ },
+ template: `
+
+ `,
+});
+
+const mountWheel = (props = {}) =>
+ mount(InvoiceCollectionSelectionActionWheel, {
+ props: {
+ selectedInvoiceCollectionIds: [],
+ totalInvoiceCollectionCount: 2,
+ allSelected: false,
+ allExpanded: false,
+ invoiceQueueBusy: false,
+ ...props,
+ },
+ global: {
+ stubs: {
+ ActionSettingsWheelButton: ActionSettingsWheelButtonStub,
+ ActionSettingsWheelItem: ActionSettingsWheelItemStub,
+ ActionSettingsWheelItemLabel: ActionSettingsWheelItemLabelStub,
+ },
+ },
+ });
+
+describe("InvoiceCollectionSelectionActionWheel", () => {
+ it("does not render without selectable invoice collections", () => {
+ const wrapper = mountWheel({ totalInvoiceCollectionCount: 0 });
+
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel"]').exists()).toBe(false);
+ });
+
+ it("shows selection controls without modification actions when nothing is selected", async () => {
+ const wrapper = mountWheel();
+
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-select-all"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-expand-all"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-invoice-selected"]').exists()).toBe(
+ false
+ );
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-merge"]').exists()).toBe(false);
+
+ await wrapper.get('[data-testid="invoice-collection-selection-action-wheel-select-all"]').trigger("click");
+ await wrapper.get('[data-testid="invoice-collection-selection-action-wheel-expand-all"]').trigger("click");
+
+ expect(wrapper.emitted("toggleSelectAll")).toHaveLength(1);
+ expect(wrapper.emitted("toggleExpandAll")).toHaveLength(1);
+ });
+
+ it("shows single-selection modification actions and hides merge", async () => {
+ const wrapper = mountWheel({ selectedInvoiceCollectionIds: [3001] });
+
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-invoice-selected"]').exists()).toBe(
+ true
+ );
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-clean-rules"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-split-by-month"]').exists()).toBe(
+ true
+ );
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-reset-hidden-prices"]').exists()).toBe(
+ true
+ );
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-merge"]').exists()).toBe(false);
+
+ await wrapper.get('[data-testid="invoice-collection-selection-action-wheel-clean-rules"]').trigger("click");
+
+ expect(wrapper.emitted("bulkAction")).toEqual([[INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES]]);
+ });
+
+ it("shows merge for multiple selections and disables invoicing while queue is busy", async () => {
+ const wrapper = mountWheel({
+ selectedInvoiceCollectionIds: [3001, 3002],
+ invoiceQueueBusy: true,
+ });
+
+ expect(
+ wrapper.get('[data-testid="invoice-collection-selection-action-wheel-invoice-selected"]').attributes("disabled")
+ ).toBeDefined();
+ expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-merge"]').exists()).toBe(true);
+
+ await wrapper.get('[data-testid="invoice-collection-selection-action-wheel-merge"]').trigger("click");
+
+ expect(wrapper.emitted("bulkAction")).toEqual([[INVOICE_COLLECTION_BULK_ACTIONS.MERGE]]);
+ });
+});
diff --git a/tests/unit/orders-table.spec.js b/tests/unit/orders-table.spec.js
index a1445846..54af1e1e 100644
--- a/tests/unit/orders-table.spec.js
+++ b/tests/unit/orders-table.spec.js
@@ -1,9 +1,13 @@
// @vitest-environment jsdom
-import { defineComponent, h, ref } from "vue";
+import { defineComponent, h, nextTick, ref } from "vue";
import { flushPromises, mount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
+import { setUseLargeTableHeaders } from "@/services/tableHeaderPreferences.js";
const getSingleMock = vi.hoisted(() => vi.fn());
+const bulkActionPreviewMock = vi.hoisted(() => vi.fn());
+const bulkActionApplyMock = vi.hoisted(() => vi.fn());
const sessionState = vi.hoisted(() => ({
canAccessSuperUser: false,
}));
@@ -30,6 +34,9 @@ const invoiceQueueState = vi.hoisted(() => {
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key) => key,
+ locale: {
+ value: "en",
+ },
}),
createI18n: () => ({
global: {
@@ -146,6 +153,10 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
get: {
single: getSingleMock,
},
+ functions: {
+ bulk_action_preview: bulkActionPreviewMock,
+ bulk_action_apply: bulkActionApplyMock,
+ },
},
global: {
language: {
@@ -178,6 +189,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
date: {
timeAgo: () => "just now",
},
+ parseErrorMessage: (error) => error?.message || String(error),
},
},
}));
@@ -261,6 +273,74 @@ const ActionSettingsWheelButtonStub = {
template: '
',
};
+const InvoiceCollectionSelectionActionWheelStub = defineComponent({
+ name: "InvoiceCollectionSelectionActionWheel",
+ props: {
+ selectedInvoiceCollectionIds: {
+ type: Array,
+ default: () => [],
+ },
+ totalInvoiceCollectionCount: {
+ type: Number,
+ default: 0,
+ },
+ allSelected: {
+ type: Boolean,
+ default: false,
+ },
+ allExpanded: {
+ type: Boolean,
+ default: false,
+ },
+ invoiceQueueBusy: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ emits: ["invoiceSelected", "bulkAction", "toggleSelectAll", "toggleExpandAll"],
+ template: `
+
+
+
+
+
+
+ `,
+});
+
+const BCheckboxStub = defineComponent({
+ name: "BCheckbox",
+ props: {
+ modelValue: {
+ type: Boolean,
+ default: false,
+ },
+ disabled: {
+ type: Boolean,
+ default: false,
+ },
+ },
+ emits: ["update:modelValue"],
+ template: `
+
+ `,
+});
+
const ActionSettingsWheelItemStub = {
template: '',
};
@@ -339,6 +419,8 @@ const mountOrdersTable = (props = {}) => {
EditableTableColumn: EditableTableColumnStub,
ColorIndicator: ColorIndicatorStub,
ActionSettingsWheelButton: ActionSettingsWheelButtonStub,
+ InvoiceCollectionSelectionActionWheel: InvoiceCollectionSelectionActionWheelStub,
+ BCheckbox: BCheckboxStub,
ActionSettingsWheelItem: ActionSettingsWheelItemStub,
OrderContentTable: OrderContentTableStub,
PosDepartmentStepMobileAttachment: PosDepartmentStepMobileAttachmentStub,
@@ -371,10 +453,26 @@ describe("OrdersTable", () => {
invoiceQueueState.success.value = [];
invoiceQueueState.queued.value = [];
invoiceQueueState.log.value = [];
+ setUseLargeTableHeaders(false);
getSingleMock.mockReset();
+ bulkActionPreviewMock.mockReset();
+ bulkActionPreviewMock.mockResolvedValue({
+ data: {
+ data: {
+ preview_id: "preview-1",
+ confirmation_phrase: "Confirm",
+ summary: {
+ changed_count: 0,
+ },
+ blockers: [],
+ },
+ },
+ });
+ bulkActionApplyMock.mockReset();
});
afterEach(() => {
+ setUseLargeTableHeaders(false);
vi.restoreAllMocks();
});
@@ -612,4 +710,148 @@ describe("OrdersTable", () => {
expect(getIndicatorForOrder(56631).attributes("data-color-class")).toBe("has-text-warning");
expect(getIndicatorForOrder(56632).attributes("data-icon")).toBe("fas fa-circle");
});
+
+ it("routes invoice collection selection controls through the action wheel", async () => {
+ const wrapper = mountOrdersTable({
+ invoiceView: true,
+ allowSelectMultiple: true,
+ autoExpandAll: false,
+ orders: [
+ createOrder({
+ id: 56633,
+ invoice_collection_id: 3001,
+ invoice_collection: { id: 3001, closed_at: null, booked_invoice_id: null },
+ }),
+ createOrder({
+ id: 56634,
+ invoice_collection_id: 3001,
+ invoice_collection: { id: 3001, closed_at: null, booked_invoice_id: null },
+ }),
+ createOrder({
+ id: 56635,
+ invoice_collection_id: 3002,
+ invoice_collection: { id: 3002, closed_at: null, booked_invoice_id: null },
+ }),
+ ],
+ });
+
+ await flushPromises();
+
+ const wheel = () => wrapper.get(".invoice-collection-selection-action-wheel-stub");
+ const firstCollectionSelector = () =>
+ wrapper.get('[data-testid="pos-order-invoice-collection-selector-56633"] input');
+ const secondSameCollectionSelector = () =>
+ wrapper.get('[data-testid="pos-order-invoice-collection-selector-56634"] input');
+ expect(wheel().attributes("data-total-count")).toBe("2");
+ expect(wheel().attributes("data-selected-count")).toBe("0");
+ expect(wheel().attributes("data-all-selected")).toBe("false");
+ expect(wheel().attributes("data-all-expanded")).toBe("false");
+ expect(wrapper.findAll("button").some((button) => /select|unselect|vælg|fravælg/i.test(button.text()))).toBe(false);
+ expect(wrapper.get('[data-testid="pos-order-invoice-collection-selector-56633"]').classes()).toContain(
+ "pos-order-invoice-collection-selector"
+ );
+
+ await firstCollectionSelector().setValue(true);
+ await flushPromises();
+
+ expect(wheel().attributes("data-selected-count")).toBe("1");
+ expect(firstCollectionSelector().element.checked).toBe(true);
+ expect(secondSameCollectionSelector().element.checked).toBe(true);
+
+ await secondSameCollectionSelector().setValue(false);
+ await flushPromises();
+
+ expect(wheel().attributes("data-selected-count")).toBe("0");
+ expect(firstCollectionSelector().element.checked).toBe(false);
+
+ await wrapper.get('[data-testid="selection-wheel-select-all"]').trigger("click");
+ await flushPromises();
+
+ expect(wheel().attributes("data-selected-count")).toBe("2");
+ expect(wheel().attributes("data-all-selected")).toBe("true");
+
+ await wrapper.get('[data-testid="selection-wheel-expand-all"]').trigger("click");
+ await flushPromises();
+
+ expect(wheel().attributes("data-all-expanded")).toBe("true");
+
+ await wrapper.get('[data-testid="selection-wheel-clean-rules"]').trigger("click");
+ await flushPromises();
+
+ expect(bulkActionPreviewMock).toHaveBeenCalledWith(
+ INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES,
+ [3001, 3002],
+ {},
+ "en"
+ );
+ });
+
+ it("uses compact table headers by default and restores large headers from the user setting", async () => {
+ const wrapper = mountOrdersTable({
+ invoiceView: true,
+ orders: [createOrder()],
+ });
+
+ await flushPromises();
+
+ expect(wrapper.find("th.pos-orders-table-header--compact").exists()).toBe(true);
+ expect(wrapper.find("th.pos-orders-table-header--vehicles.pos-orders-table-header--compact").exists()).toBe(true);
+ expect(wrapper.find("th .pleno-table-header-content").exists()).toBe(true);
+
+ setUseLargeTableHeaders(true);
+ await nextTick();
+
+ expect(wrapper.find("th.pos-orders-table-header--compact").exists()).toBe(false);
+ expect(wrapper.find("th.pos-orders-table-header--vehicles").exists()).toBe(true);
+ });
+
+ it("renders stacked desktop registration numbers in the compact vehicle column style", async () => {
+ const wrapper = mountOrdersTable({
+ invoiceView: true,
+ orders: [
+ createOrder({
+ reg_1: "AB12345",
+ reg_2: "CD67890",
+ reg_3: "EF24680",
+ }),
+ ],
+ });
+
+ await flushPromises();
+
+ const vehicleCell = wrapper.get(".pos-order-vehicle-cell");
+ const registrationLines = vehicleCell.findAll(".pos-order-vehicle-registration-line");
+
+ expect(registrationLines).toHaveLength(3);
+ expect(registrationLines.map((line) => line.text())).toEqual(["AB12345", "CD67890", "EF24680"]);
+ });
+
+ it("centers the desktop ID status indicator group in the ID cell", async () => {
+ const wrapper = mountOrdersTable({
+ invoiceView: true,
+ orders: [createOrder({ id: 54518 })],
+ });
+
+ await flushPromises();
+
+ const idCell = wrapper.get(".pos-orders-table-id-cell");
+ expect(idCell.classes()).toContain("is-narrow");
+ expect(idCell.text()).toContain("54518");
+ expect(idCell.find(".pos-orders-table-id-cell-content").exists()).toBe(true);
+ expect(idCell.find(".color-indicator-stub").exists()).toBe(true);
+ });
+
+ it("uses row-level alignment classes for the desktop orders table row", async () => {
+ const wrapper = mountOrdersTable({
+ invoiceView: true,
+ orders: [createOrder({ id: 54518 })],
+ });
+
+ await flushPromises();
+
+ const row = wrapper.get(".pos-orders-table-row");
+ expect(row.find(".pos-orders-table-id-cell").exists()).toBe(true);
+ expect(row.find(".pos-order-vehicle-cell").exists()).toBe(true);
+ expect(row.find(".pos-orders-table-actions-cell").exists()).toBe(true);
+ });
});
diff --git a/tests/unit/table-header-preferences.spec.js b/tests/unit/table-header-preferences.spec.js
new file mode 100644
index 00000000..bc24951d
--- /dev/null
+++ b/tests/unit/table-header-preferences.spec.js
@@ -0,0 +1,34 @@
+// @vitest-environment jsdom
+import { beforeEach, describe, expect, it } from "vitest";
+import {
+ LARGE_TABLE_HEADERS_BODY_CLASS,
+ LARGE_TABLE_HEADERS_STORAGE_KEY,
+ setUseLargeTableHeaders,
+ useLargeTableHeaders,
+} from "@/services/tableHeaderPreferences.js";
+
+describe("table header preferences", () => {
+ beforeEach(() => {
+ window.localStorage.removeItem(LARGE_TABLE_HEADERS_STORAGE_KEY);
+ setUseLargeTableHeaders(false);
+ });
+
+ it("uses compact table headers by default", () => {
+ expect(useLargeTableHeaders.value).toBe(false);
+ expect(document.body.classList.contains(LARGE_TABLE_HEADERS_BODY_CLASS)).toBe(false);
+ });
+
+ it("toggles the document class for large table headers", () => {
+ setUseLargeTableHeaders(true);
+
+ expect(window.localStorage.getItem(LARGE_TABLE_HEADERS_STORAGE_KEY)).toBe("true");
+ expect(useLargeTableHeaders.value).toBe(true);
+ expect(document.body.classList.contains(LARGE_TABLE_HEADERS_BODY_CLASS)).toBe(true);
+
+ setUseLargeTableHeaders(false);
+
+ expect(window.localStorage.getItem(LARGE_TABLE_HEADERS_STORAGE_KEY)).toBe("false");
+ expect(useLargeTableHeaders.value).toBe(false);
+ expect(document.body.classList.contains(LARGE_TABLE_HEADERS_BODY_CLASS)).toBe(false);
+ });
+});