diff --git a/src/components/displays/department/pos/orders/ordersTable.vue b/src/components/displays/department/pos/orders/ordersTable.vue
index dd3244b7..9a29d2e1 100644
--- a/src/components/displays/department/pos/orders/ordersTable.vue
+++ b/src/components/displays/department/pos/orders/ordersTable.vue
@@ -12,6 +12,10 @@ import InvoicingPeriodFlagList from "@/views/dashboards/superUserDashboard/Invoi
import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
import SuperuserInvoiceRowActions from "@/components/displays/superuser/tables/SuperuserInvoiceRowActions.vue";
import { useLargeTableHeaders } from "@/services/tableHeaderPreferences.js";
+import {
+ normalizeInvoiceCollectionActionPreview,
+ renderInvoiceCollectionActionPreviewHtml,
+} from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoiceCollectionActionPreview.ts";
const props = defineProps({
orders: {
@@ -218,14 +222,6 @@ const normalizeInvoiceCollectionId = (invoiceCollectionId) => {
const getApiPayload = (response) => response?.data?.data ?? response?.data ?? response ?? {};
-const escapeHtml = (value) =>
- String(value ?? "")
- .replace(/&/g, "&")
- .replace(//g, ">")
- .replace(/"/g, """)
- .replace(/'/g, "'");
-
const getInvoiceCollectionResponseOrders = (response) => {
if (Array.isArray(response?.orders)) {
return response.orders;
@@ -503,84 +499,11 @@ const selectableInvoiceCollectionCount = computed(() => getUniqueInvoiceCollecti
const getBulkActionLabel = (action) => t(`invoicing_period.invoice_collection_actions.actions.${action}`);
const renderBulkActionPreviewHtml = (preview) => {
- const summary = preview?.summary || {};
- const collections = Number(summary.collections ?? preview?.invoice_collection_ids?.length ?? 0);
- const changedCount = Number(summary.changed_count ?? summary.order_items ?? summary.orders_to_move ?? 0);
- const skippedCount = Number(summary.skipped_count ?? 0);
- const blockers = Array.isArray(preview?.blockers) ? preview.blockers : [];
- const orderItems = Array.isArray(preview?.order_items) ? preview.order_items : [];
- const orders = Array.isArray(preview?.orders) ? preview.orders : [];
- const splitItems = Array.isArray(preview?.items) ? preview.items : [];
- const detailItems = [
- ...orderItems.map((item) =>
- t("invoicing_period.invoice_collection_actions.preview.order_item_line", {
- collection: item.invoice_collection_id,
- order: item.order_id,
- product: item.product_name || item.product_id,
- })
- ),
- ...orders.map((item) =>
- t("invoicing_period.invoice_collection_actions.preview.order_move_line", {
- order: item.order_id,
- source: item.source_invoice_collection_id,
- target: item.target_invoice_collection_id,
- })
- ),
- ...splitItems.map((item) =>
- t("invoicing_period.invoice_collection_actions.preview.collection_status_line", {
- collection: item.invoice_collection_id,
- status: item.status,
- })
- ),
- ].slice(0, 10);
-
- const summaryLines = [
- t("invoicing_period.invoice_collection_actions.preview.collections", { count: collections }),
- t("invoicing_period.invoice_collection_actions.preview.changed", { count: changedCount }),
- ...(skippedCount > 0
- ? [t("invoicing_period.invoice_collection_actions.preview.skipped", { count: skippedCount })]
- : []),
- ...(preview?.target_invoice_collection_id
- ? [
- t("invoicing_period.invoice_collection_actions.preview.merge_target", {
- id: preview.target_invoice_collection_id,
- }),
- ]
- : []),
- ];
-
- const html = [
- `
${escapeHtml(
- t("invoicing_period.invoice_collection_actions.preview.requires_confirmation", {
- phrase: preview?.confirmation_phrase || "",
- })
- )}
`,
- `${summaryLines.map((line) => `- ${escapeHtml(line)}
`).join("")}
`,
- ];
-
- if (detailItems.length > 0) {
- html.push(
- `
${escapeHtml(
- t("invoicing_period.invoice_collection_actions.preview.affected_examples")
- )}
`
- );
- html.push(`${detailItems.map((line) => `- ${escapeHtml(line)}
`).join("")}
`);
- }
-
- if (blockers.length > 0) {
- html.push(
- `
${escapeHtml(
- t("invoicing_period.invoice_collection_actions.preview.blockers")
- )}
`
- );
- html.push(
- `${blockers
- .map((blocker) => `- ${escapeHtml(blocker.message || blocker.code)}
`)
- .join("")}
`
- );
- }
-
- return html.join("");
+ return renderInvoiceCollectionActionPreviewHtml({
+ preview: normalizeInvoiceCollectionActionPreview(preview, getSelectedInvoiceCollectionIds().length),
+ t,
+ formatCurrency: (value) => SessionUser.functions.currency.toLocal(value),
+ });
};
const chooseMergeTargetInvoiceCollection = async (invoiceCollectionIds) => {
diff --git a/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue b/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue
index 7025b1c1..3ab6ec60 100644
--- a/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue
+++ b/src/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue
@@ -516,12 +516,24 @@ export const CollectedOrderInvoices = {
throw error;
});
},
- bulk_action_preview: async (action, invoiceCollectionIds = [], options = {}, locale = null) => {
+ bulk_action_preview: async (
+ action,
+ invoiceCollectionIds = [],
+ options = {},
+ locale = null,
+ snapshotBinding = {}
+ ) => {
return authenticatedRequest('/collected-invoices/bulk-actions/preview', 'POST', {
action,
invoice_collection_ids: invoiceCollectionIds.map((id) => parseInt(id)).filter((id) => Number.isInteger(id) && id > 0),
options,
...(locale ? { locale } : {}),
+ ...(Number.isInteger(parseInt(snapshotBinding.customer_number))
+ ? { customer_number: parseInt(snapshotBinding.customer_number) }
+ : {}),
+ ...(snapshotBinding.snapshot_revision
+ ? { snapshot_revision: String(snapshotBinding.snapshot_revision) }
+ : {}),
}).then((response) => {
console.log(response);
return response;
@@ -537,6 +549,8 @@ export const CollectedOrderInvoices = {
options = {},
confirmation_text,
locale = null,
+ customer_number = null,
+ snapshot_revision = null,
}) => {
return authenticatedRequest('/collected-invoices/bulk-actions/apply', 'POST', {
preview_id,
@@ -545,6 +559,8 @@ export const CollectedOrderInvoices = {
options,
confirmation_text,
...(locale ? { locale } : {}),
+ ...(Number.isInteger(parseInt(customer_number)) ? { customer_number: parseInt(customer_number) } : {}),
+ ...(snapshot_revision ? { snapshot_revision: String(snapshot_revision) } : {}),
}).then((response) => {
console.log(response);
return response;
@@ -553,6 +569,28 @@ export const CollectedOrderInvoices = {
throw error;
});
},
+ period_tree_action_preview: async ({
+ action,
+ invoice_collection_ids = [],
+ options = {},
+ customer_number,
+ snapshot_revision,
+ locale = null,
+ }) => authenticatedRequest('/superuser/invoicing/period/tree-actions/preview', 'POST', {
+ action,
+ invoice_collection_ids: invoice_collection_ids
+ .map((id) => parseInt(id))
+ .filter((id) => Number.isInteger(id) && id > 0),
+ options,
+ customer_number: parseInt(customer_number),
+ snapshot_revision: String(snapshot_revision || ''),
+ ...(locale ? { locale } : {}),
+ }),
+ period_tree_action_apply: async ({ preview_id, confirmation_text }) =>
+ authenticatedRequest('/superuser/invoicing/period/tree-actions/apply', 'POST', {
+ preview_id: String(preview_id || ''),
+ confirmation_text: String(confirmation_text || ''),
+ }),
move_to_customer: async (id, customerNumber) => {
const invoiceCollectionId = parseInt(id);
const targetCustomerNumber = parseInt(customerNumber);
diff --git a/src/i18n/generated/da-v2.json b/src/i18n/generated/da-v2.json
index 9cd6fa50..fe1e047c 100644
--- a/src/i18n/generated/da-v2.json
+++ b/src/i18n/generated/da-v2.json
@@ -4310,6 +4310,7 @@
"no_changes_title": "Ingen ændringer",
"order_item_line": "Fakturasamling #{collection}, ordre #{order}: {product}",
"order_move_line": "Ordre #{order}: #{source} til #{target}",
+ "off_period_impact": "Uden for valgt periode: {count} orders · {amount}",
"requires_confirmation": "Gennemgå forhåndsvisningen. Ingen ændringer udføres før du skriver {phrase}.",
"skipped": "Sprunget over: {count}",
"title": "Forhåndsvis {action}"
@@ -4500,6 +4501,7 @@
},
"nodes": {
"attachment_fallback": "Vedhæftning #{id}",
+ "agreement": "Aftale #{id}",
"booking": "Booking #{id}",
"booking_item_fallback": "Bookinglinje #{id}",
"collection": "Fakturasamling #{id}",
@@ -4512,6 +4514,7 @@
"order_item_fallback": "Linje #{id}",
"orders_without_collection": "Orders uden fakturasamling",
"payment_for_order": "Kortbetaling for ordre #{id}",
+ "payment": "Betaling #{id}",
"vehicle_subscription": "Vaskeabonnement",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -4527,9 +4530,11 @@
"loading": "Indlæser preview"
},
"subtitles": {
+ "complete_collection_summary": "Hele samlingen: {count} orders · {amount}",
"collection": "Samling #{id}",
"order": "Vask #{id}",
"quantity": "Antal {count}",
+ "selected_period_summary": "Valgt periode: {count} orders · {amount}",
"wash_id": "WashId {id}"
},
"success": {
diff --git a/src/i18n/generated/de-v2.json b/src/i18n/generated/de-v2.json
index 21bdce6f..44d23528 100644
--- a/src/i18n/generated/de-v2.json
+++ b/src/i18n/generated/de-v2.json
@@ -4420,6 +4420,7 @@
"no_changes_title": "Keine Änderungen",
"order_item_line": "Rechnungssammlung #{collection}, Auftrag #{order}: {product}",
"order_move_line": "Auftrag #{order}: #{source} nach #{target}",
+ "off_period_impact": "Außerhalb des ausgewählten Zeitraums: {count} Aufträge · {amount}",
"requires_confirmation": "Prüfen Sie die Vorschau. Es werden keine Änderungen ausgeführt, bevor Sie {phrase} eingeben.",
"skipped": "Übersprungen: {count}",
"title": "{action} Vorschau"
@@ -4610,6 +4611,7 @@
},
"nodes": {
"attachment_fallback": "Anhang #{id}",
+ "agreement": "Vereinbarung #{id}",
"booking": "Buchung #{id}",
"booking_item_fallback": "Buchungsposition #{id}",
"collection": "Fakturasammlung #{id}",
@@ -4622,6 +4624,7 @@
"order_item_fallback": "Position #{id}",
"orders_without_collection": "Orders ohne Fakturasammlung",
"payment_for_order": "Kartenzahlung für Auftrag #{id}",
+ "payment": "Zahlung #{id}",
"vehicle_subscription": "Waschabonnement",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -4637,9 +4640,11 @@
"loading": "Vorschau wird geladen"
},
"subtitles": {
+ "complete_collection_summary": "Gesamte Sammlung: {count} Aufträge · {amount}",
"collection": "Sammlung #{id}",
"order": "Auftrag #{id}",
"quantity": "Anzahl {count}",
+ "selected_period_summary": "Ausgewählter Zeitraum: {count} Aufträge · {amount}",
"wash_id": "WashId {id}"
},
"success": {
diff --git a/src/i18n/generated/en-v2.json b/src/i18n/generated/en-v2.json
index e1017a1b..d1be3a36 100644
--- a/src/i18n/generated/en-v2.json
+++ b/src/i18n/generated/en-v2.json
@@ -4141,6 +4141,7 @@
"no_changes_title": "No changes",
"order_item_line": "Invoice collection #{collection}, order #{order}: {product}",
"order_move_line": "Order #{order}: #{source} to #{target}",
+ "off_period_impact": "Outside selected period: {count} orders · {amount}",
"requires_confirmation": "Review the preview. No changes are applied until you type {phrase}.",
"skipped": "Skipped: {count}",
"title": "Preview {action}"
@@ -4331,6 +4332,7 @@
},
"nodes": {
"attachment_fallback": "Attachment #{id}",
+ "agreement": "Agreement #{id}",
"booking": "Booking #{id}",
"booking_item_fallback": "Booking item #{id}",
"collection": "Invoice collection #{id}",
@@ -4343,6 +4345,7 @@
"order_item_fallback": "Line #{id}",
"orders_without_collection": "Orders without invoice collection",
"payment_for_order": "Card payment for order #{id}",
+ "payment": "Payment #{id}",
"vehicle_subscription": "Wash subscription",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -4358,9 +4361,11 @@
"loading": "Loading preview"
},
"subtitles": {
+ "complete_collection_summary": "Whole collection: {count} orders · {amount}",
"collection": "Collection #{id}",
"order": "Wash #{id}",
"quantity": "Quantity {count}",
+ "selected_period_summary": "Selected period: {count} orders · {amount}",
"wash_id": "WashId {id}"
},
"success": {
diff --git a/src/i18n/generated/global-v2.json b/src/i18n/generated/global-v2.json
index 10cdd4bc..f75313de 100644
--- a/src/i18n/generated/global-v2.json
+++ b/src/i18n/generated/global-v2.json
@@ -3529,6 +3529,7 @@
"no_changes_title": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.no_changes_title'}",
"order_item_line": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.order_item_line'}",
"order_move_line": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.order_move_line'}",
+ "off_period_impact": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.off_period_impact'}",
"requires_confirmation": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.requires_confirmation'}",
"skipped": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.skipped'}",
"title": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.title'}"
@@ -3717,6 +3718,7 @@
},
"nodes": {
"attachment_fallback": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.attachment_fallback'}",
+ "agreement": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.agreement'}",
"booking": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.booking'}",
"booking_item_fallback": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.booking_item_fallback'}",
"collection": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.collection'}",
@@ -3729,6 +3731,7 @@
"order_item_fallback": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.order_item_fallback'}",
"orders_without_collection": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.orders_without_collection'}",
"payment_for_order": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.payment_for_order'}",
+ "payment": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.payment'}",
"vehicle_subscription": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.vehicle_subscription'}",
"xlvask": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.xlvask'}",
"xlvask_empty": "@:{'templates.generated.compat.invoicing_period.object_tree.nodes.xlvask_empty'}",
@@ -3744,9 +3747,11 @@
"loading": "@:{'templates.generated.compat.invoicing_period.object_tree.preview.loading'}"
},
"subtitles": {
+ "complete_collection_summary": "@:{'templates.generated.compat.invoicing_period.object_tree.subtitles.complete_collection_summary'}",
"collection": "@:{'templates.generated.compat.invoicing_period.object_tree.subtitles.collection'}",
"order": "@:{'templates.generated.compat.invoicing_period.object_tree.subtitles.order'}",
"quantity": "@:{'templates.generated.compat.invoicing_period.object_tree.subtitles.quantity'}",
+ "selected_period_summary": "@:{'templates.generated.compat.invoicing_period.object_tree.subtitles.selected_period_summary'}",
"wash_id": "@:{'templates.generated.compat.invoicing_period.object_tree.subtitles.wash_id'}"
},
"success": {
diff --git a/src/i18n/generated/no-v2.json b/src/i18n/generated/no-v2.json
index f29c6d06..18e5381a 100644
--- a/src/i18n/generated/no-v2.json
+++ b/src/i18n/generated/no-v2.json
@@ -4423,6 +4423,7 @@
"no_changes_title": "Ingen endringer",
"order_item_line": "Fakturasamling #{collection}, ordre #{order}: {product}",
"order_move_line": "Ordre #{order}: #{source} til #{target}",
+ "off_period_impact": "Utenfor valgt periode: {count} ordrer · {amount}",
"requires_confirmation": "Gå gjennom forhåndsvisningen. Ingen endringer utføres før du skriver {phrase}.",
"skipped": "Hoppet over: {count}",
"title": "Forhåndsvis {action}"
@@ -4613,6 +4614,7 @@
},
"nodes": {
"attachment_fallback": "Vedlegg #{id}",
+ "agreement": "Avtale #{id}",
"booking": "Booking #{id}",
"booking_item_fallback": "Bookinglinje #{id}",
"collection": "Fakturasamling #{id}",
@@ -4625,6 +4627,7 @@
"order_item_fallback": "Linje #{id}",
"orders_without_collection": "Orders uten fakturasamling",
"payment_for_order": "Kortbetaling for ordre #{id}",
+ "payment": "Betaling #{id}",
"vehicle_subscription": "Vaskeabonnement",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -4640,9 +4643,11 @@
"loading": "Laster forhåndsvisning"
},
"subtitles": {
+ "complete_collection_summary": "Hele samlingen: {count} ordrer · {amount}",
"collection": "Samling #{id}",
"order": "Ordre #{id}",
"quantity": "Antall {count}",
+ "selected_period_summary": "Valgt periode: {count} ordrer · {amount}",
"wash_id": "WashId {id}"
},
"success": {
diff --git a/src/i18n/generated/sv-v2.json b/src/i18n/generated/sv-v2.json
index 36d343e0..2a11376c 100644
--- a/src/i18n/generated/sv-v2.json
+++ b/src/i18n/generated/sv-v2.json
@@ -4473,6 +4473,7 @@
"no_changes_title": "Inga ändringar",
"order_item_line": "Fakturasamling #{collection}, order #{order}: {product}",
"order_move_line": "Order #{order}: #{source} till #{target}",
+ "off_period_impact": "Utanför vald period: {count} ordrar · {amount}",
"requires_confirmation": "Granska förhandsvisningen. Inga ändringar görs innan du skriver {phrase}.",
"skipped": "Hoppade över: {count}",
"title": "Förhandsvisa {action}"
@@ -4663,6 +4664,7 @@
},
"nodes": {
"attachment_fallback": "Bilaga #{id}",
+ "agreement": "Avtal #{id}",
"booking": "Bokning #{id}",
"booking_item_fallback": "Bokningsrad #{id}",
"collection": "Fakturasamling #{id}",
@@ -4675,6 +4677,7 @@
"order_item_fallback": "Rad #{id}",
"orders_without_collection": "Orders utan fakturasamling",
"payment_for_order": "Kortbetalning för order #{id}",
+ "payment": "Betalning #{id}",
"vehicle_subscription": "Tvättabonnemang",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -4690,9 +4693,11 @@
"loading": "Läser in förhandsvisning"
},
"subtitles": {
+ "complete_collection_summary": "Hela samlingen: {count} ordrar · {amount}",
"collection": "Samling #{id}",
"order": "Order #{id}",
"quantity": "Antal {count}",
+ "selected_period_summary": "Vald period: {count} ordrar · {amount}",
"wash_id": "WashId {id}"
},
"success": {
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 f7791772..98563449 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
@@ -36,6 +36,7 @@
"no_changes_title": "Ingen ændringer",
"order_item_line": "Fakturasamling #{collection}, ordre #{order}: {product}",
"order_move_line": "Ordre #{order}: #{source} til #{target}",
+ "off_period_impact": "Uden for valgt periode: {count} orders · {amount}",
"requires_confirmation": "Gennemgå forhåndsvisningen. Ingen ændringer udføres før du skriver {phrase}.",
"skipped": "Sprunget over: {count}",
"title": "Forhåndsvis {action}"
diff --git a/src/i18n/source/da/phrases/compat/invoicing_period/object_tree.json b/src/i18n/source/da/phrases/compat/invoicing_period/object_tree.json
index 6155bbef..a87da8bc 100644
--- a/src/i18n/source/da/phrases/compat/invoicing_period/object_tree.json
+++ b/src/i18n/source/da/phrases/compat/invoicing_period/object_tree.json
@@ -158,6 +158,7 @@
},
"nodes": {
"attachment_fallback": "Vedhæftning #{id}",
+ "agreement": "Aftale #{id}",
"booking": "Booking #{id}",
"booking_item_fallback": "Bookinglinje #{id}",
"collection": "Fakturasamling #{id}",
@@ -170,6 +171,7 @@
"order_item_fallback": "Linje #{id}",
"orders_without_collection": "Orders uden fakturasamling",
"payment_for_order": "Kortbetaling for ordre #{id}",
+ "payment": "Betaling #{id}",
"vehicle_subscription": "Vaskeabonnement",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -185,9 +187,11 @@
"loading": "Indlæser preview"
},
"subtitles": {
+ "complete_collection_summary": "Hele samlingen: {count} orders · {amount}",
"collection": "Samling #{id}",
"order": "Vask #{id}",
"quantity": "Antal {count}",
+ "selected_period_summary": "Valgt periode: {count} orders · {amount}",
"wash_id": "WashId {id}"
},
"success": {
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 a805b2a9..a12f637e 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
@@ -36,6 +36,7 @@
"no_changes_title": "Keine Änderungen",
"order_item_line": "Rechnungssammlung #{collection}, Auftrag #{order}: {product}",
"order_move_line": "Auftrag #{order}: #{source} nach #{target}",
+ "off_period_impact": "Außerhalb des ausgewählten Zeitraums: {count} Aufträge · {amount}",
"requires_confirmation": "Prüfen Sie die Vorschau. Es werden keine Änderungen ausgeführt, bevor Sie {phrase} eingeben.",
"skipped": "Übersprungen: {count}",
"title": "{action} Vorschau"
diff --git a/src/i18n/source/de/phrases/compat/invoicing_period/object_tree.json b/src/i18n/source/de/phrases/compat/invoicing_period/object_tree.json
index 5aea98a6..462d4f88 100644
--- a/src/i18n/source/de/phrases/compat/invoicing_period/object_tree.json
+++ b/src/i18n/source/de/phrases/compat/invoicing_period/object_tree.json
@@ -158,6 +158,7 @@
},
"nodes": {
"attachment_fallback": "Anhang #{id}",
+ "agreement": "Vereinbarung #{id}",
"booking": "Buchung #{id}",
"booking_item_fallback": "Buchungsposition #{id}",
"collection": "Fakturasammlung #{id}",
@@ -170,6 +171,7 @@
"order_item_fallback": "Position #{id}",
"orders_without_collection": "Orders ohne Fakturasammlung",
"payment_for_order": "Kartenzahlung für Auftrag #{id}",
+ "payment": "Zahlung #{id}",
"vehicle_subscription": "Waschabonnement",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -185,9 +187,11 @@
"loading": "Vorschau wird geladen"
},
"subtitles": {
+ "complete_collection_summary": "Gesamte Sammlung: {count} Aufträge · {amount}",
"collection": "Sammlung #{id}",
"order": "Auftrag #{id}",
"quantity": "Anzahl {count}",
+ "selected_period_summary": "Ausgewählter Zeitraum: {count} Aufträge · {amount}",
"wash_id": "WashId {id}"
},
"success": {
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 2f804a85..e29857e6 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
@@ -36,6 +36,7 @@
"no_changes_title": "No changes",
"order_item_line": "Invoice collection #{collection}, order #{order}: {product}",
"order_move_line": "Order #{order}: #{source} to #{target}",
+ "off_period_impact": "Outside selected period: {count} orders · {amount}",
"requires_confirmation": "Review the preview. No changes are applied until you type {phrase}.",
"skipped": "Skipped: {count}",
"title": "Preview {action}"
diff --git a/src/i18n/source/en/phrases/compat/invoicing_period/object_tree.json b/src/i18n/source/en/phrases/compat/invoicing_period/object_tree.json
index f91493a3..cbe1ae0a 100644
--- a/src/i18n/source/en/phrases/compat/invoicing_period/object_tree.json
+++ b/src/i18n/source/en/phrases/compat/invoicing_period/object_tree.json
@@ -158,6 +158,7 @@
},
"nodes": {
"attachment_fallback": "Attachment #{id}",
+ "agreement": "Agreement #{id}",
"booking": "Booking #{id}",
"booking_item_fallback": "Booking item #{id}",
"collection": "Invoice collection #{id}",
@@ -170,6 +171,7 @@
"order_item_fallback": "Line #{id}",
"orders_without_collection": "Orders without invoice collection",
"payment_for_order": "Card payment for order #{id}",
+ "payment": "Payment #{id}",
"vehicle_subscription": "Wash subscription",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -185,9 +187,11 @@
"loading": "Loading preview"
},
"subtitles": {
+ "complete_collection_summary": "Whole collection: {count} orders · {amount}",
"collection": "Collection #{id}",
"order": "Wash #{id}",
"quantity": "Quantity {count}",
+ "selected_period_summary": "Selected period: {count} orders · {amount}",
"wash_id": "WashId {id}"
},
"success": {
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 1e209525..7b313540 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
@@ -35,6 +35,7 @@
"no_changes_title": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.no_changes_title'}",
"order_item_line": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.order_item_line'}",
"order_move_line": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.order_move_line'}",
+ "off_period_impact": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.off_period_impact'}",
"requires_confirmation": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.requires_confirmation'}",
"skipped": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.skipped'}",
"title": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.title'}"
diff --git a/src/i18n/source/global/shared/invoicing_period/object_tree.json b/src/i18n/source/global/shared/invoicing_period/object_tree.json
index 11161bbb..bbde25ea 100644
--- a/src/i18n/source/global/shared/invoicing_period/object_tree.json
+++ b/src/i18n/source/global/shared/invoicing_period/object_tree.json
@@ -154,6 +154,7 @@
},
"nodes": {
"attachment_fallback": "@:{'phrases.compat.invoicing_period.object_tree.nodes.attachment_fallback'}",
+ "agreement": "@:{'phrases.compat.invoicing_period.object_tree.nodes.agreement'}",
"booking": "@:{'phrases.compat.invoicing_period.object_tree.nodes.booking'}",
"booking_item_fallback": "@:{'phrases.compat.invoicing_period.object_tree.nodes.booking_item_fallback'}",
"collection": "@:{'phrases.compat.invoicing_period.object_tree.nodes.collection'}",
@@ -166,6 +167,7 @@
"order_item_fallback": "@:{'phrases.compat.invoicing_period.object_tree.nodes.order_item_fallback'}",
"orders_without_collection": "@:{'phrases.compat.invoicing_period.object_tree.nodes.orders_without_collection'}",
"payment_for_order": "@:{'phrases.compat.invoicing_period.object_tree.nodes.payment_for_order'}",
+ "payment": "@:{'phrases.compat.invoicing_period.object_tree.nodes.payment'}",
"vehicle_subscription": "@:{'phrases.compat.invoicing_period.object_tree.nodes.vehicle_subscription'}",
"xlvask": "@:{'phrases.compat.invoicing_period.object_tree.nodes.xlvask'}",
"xlvask_empty": "@:{'phrases.compat.invoicing_period.object_tree.nodes.xlvask_empty'}",
@@ -181,9 +183,11 @@
"loading": "@:{'phrases.compat.invoicing_period.object_tree.preview.loading'}"
},
"subtitles": {
+ "complete_collection_summary": "@:{'phrases.compat.invoicing_period.object_tree.subtitles.complete_collection_summary'}",
"collection": "@:{'phrases.compat.invoicing_period.object_tree.subtitles.collection'}",
"order": "@:{'phrases.compat.invoicing_period.object_tree.subtitles.order'}",
"quantity": "@:{'phrases.compat.invoicing_period.object_tree.subtitles.quantity'}",
+ "selected_period_summary": "@:{'phrases.compat.invoicing_period.object_tree.subtitles.selected_period_summary'}",
"wash_id": "@:{'phrases.compat.invoicing_period.object_tree.subtitles.wash_id'}"
},
"success": {
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 7aed0a47..cb7af6c1 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
@@ -36,6 +36,7 @@
"no_changes_title": "Ingen endringer",
"order_item_line": "Fakturasamling #{collection}, ordre #{order}: {product}",
"order_move_line": "Ordre #{order}: #{source} til #{target}",
+ "off_period_impact": "Utenfor valgt periode: {count} ordrer · {amount}",
"requires_confirmation": "Gå gjennom forhåndsvisningen. Ingen endringer utføres før du skriver {phrase}.",
"skipped": "Hoppet over: {count}",
"title": "Forhåndsvis {action}"
diff --git a/src/i18n/source/no/phrases/compat/invoicing_period/object_tree.json b/src/i18n/source/no/phrases/compat/invoicing_period/object_tree.json
index 366077b1..13ad8680 100644
--- a/src/i18n/source/no/phrases/compat/invoicing_period/object_tree.json
+++ b/src/i18n/source/no/phrases/compat/invoicing_period/object_tree.json
@@ -158,6 +158,7 @@
},
"nodes": {
"attachment_fallback": "Vedlegg #{id}",
+ "agreement": "Avtale #{id}",
"booking": "Booking #{id}",
"booking_item_fallback": "Bookinglinje #{id}",
"collection": "Fakturasamling #{id}",
@@ -170,6 +171,7 @@
"order_item_fallback": "Linje #{id}",
"orders_without_collection": "Orders uten fakturasamling",
"payment_for_order": "Kortbetaling for ordre #{id}",
+ "payment": "Betaling #{id}",
"vehicle_subscription": "Vaskeabonnement",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -185,9 +187,11 @@
"loading": "Laster forhåndsvisning"
},
"subtitles": {
+ "complete_collection_summary": "Hele samlingen: {count} ordrer · {amount}",
"collection": "Samling #{id}",
"order": "Ordre #{id}",
"quantity": "Antall {count}",
+ "selected_period_summary": "Valgt periode: {count} ordrer · {amount}",
"wash_id": "WashId {id}"
},
"success": {
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 af91628f..d1721432 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
@@ -36,6 +36,7 @@
"no_changes_title": "Inga ändringar",
"order_item_line": "Fakturasamling #{collection}, order #{order}: {product}",
"order_move_line": "Order #{order}: #{source} till #{target}",
+ "off_period_impact": "Utanför vald period: {count} ordrar · {amount}",
"requires_confirmation": "Granska förhandsvisningen. Inga ändringar görs innan du skriver {phrase}.",
"skipped": "Hoppade över: {count}",
"title": "Förhandsvisa {action}"
diff --git a/src/i18n/source/sv/phrases/compat/invoicing_period/object_tree.json b/src/i18n/source/sv/phrases/compat/invoicing_period/object_tree.json
index 521c42ce..c81c386f 100644
--- a/src/i18n/source/sv/phrases/compat/invoicing_period/object_tree.json
+++ b/src/i18n/source/sv/phrases/compat/invoicing_period/object_tree.json
@@ -158,6 +158,7 @@
},
"nodes": {
"attachment_fallback": "Bilaga #{id}",
+ "agreement": "Avtal #{id}",
"booking": "Bokning #{id}",
"booking_item_fallback": "Bokningsrad #{id}",
"collection": "Fakturasamling #{id}",
@@ -170,6 +171,7 @@
"order_item_fallback": "Rad #{id}",
"orders_without_collection": "Orders utan fakturasamling",
"payment_for_order": "Kortbetalning för order #{id}",
+ "payment": "Betalning #{id}",
"vehicle_subscription": "Tvättabonnemang",
"xlvask": "XL Vask #{id}",
"xlvask_empty": "XL Vask",
@@ -185,9 +187,11 @@
"loading": "Läser in förhandsvisning"
},
"subtitles": {
+ "complete_collection_summary": "Hela samlingen: {count} ordrar · {amount}",
"collection": "Samling #{id}",
"order": "Order #{id}",
"quantity": "Antal {count}",
+ "selected_period_summary": "Vald period: {count} ordrar · {amount}",
"wash_id": "WashId {id}"
},
"success": {
diff --git a/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue b/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue
index 63a69510..8f3c89c2 100644
--- a/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue
+++ b/src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue
@@ -10,6 +10,17 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { getAttachmentPreviewKind, releaseObjectUrl } from "@/services/attachmentPreview.js";
import { invoiceQueue } from "../imports/InvoicingBillingPeriodImportInvoiceQueue.vue";
import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
+import {
+ normalizeInvoiceCollectionActionPreview,
+ renderInvoiceCollectionActionPreviewHtml,
+ isInvoiceCollectionTreeActionUnavailable,
+} from "../services/invoiceCollectionActionPreview.ts";
+import {
+ buildCompleteSnapshotRootNodes,
+ createInvoicingPeriodTreeSnapshotLoader,
+ isTreeEndpointUnavailable,
+ type CompleteInvoicingPeriodTreeSnapshot,
+} from "../services/invoicingPeriodTreeSnapshot.ts";
import {
TREE_CATEGORY_TYPES,
TREE_NODE_TYPES,
@@ -36,6 +47,7 @@ const props = withDefaults(
transactions: any[];
excludedOrderIds?: any[];
dates: { dateFrom: string; dateTo: string };
+ capabilities?: Record;
invoicePeriodFlags?: any[];
autoExpandAll?: boolean;
}>(),
@@ -43,6 +55,7 @@ const props = withDefaults(
transactions: () => [],
excludedOrderIds: () => [],
invoicePeriodFlags: () => [],
+ capabilities: () => ({}),
autoExpandAll: false,
}
);
@@ -67,6 +80,13 @@ const previewErrorByNodeId = ref>({});
const attachmentPreviewByNodeId = ref>({});
const economicDetailsByCollectionId = ref>({});
const generatedPreviewObjectUrls = new Set();
+const activeSnapshot = ref(null);
+const snapshotLoading = ref(false);
+const snapshotFallback = ref(false);
+const legacySnapshotFallbackAllowed = ref(false);
+const treeDataVersion = ref(0);
+const snapshotLoader = createInvoicingPeriodTreeSnapshotLoader(SessionUser.request);
+let snapshotRequestGeneration = 0;
const toPositiveInteger = (value: any) => {
const parsed = Number.parseInt(String(value ?? ""), 10);
@@ -138,16 +158,28 @@ const nodeLabel = {
treeText("nodes.xlvask_item_fallback", `XL Vask linje ${index + 1}`, { index: index + 1 }),
};
-const currentSignature = computed(() =>
+const legacyDataSignature = computed(() =>
JSON.stringify({
customer: props.customer?.customer_number,
- ids: props.transactions.map((transaction) => transaction?.id),
+ transactions: props.transactions.map((transaction) => ({
+ id: transaction?.id,
+ collectionId: transaction?.invoice_collection_id,
+ amount: transaction?.total_net_amount ?? transaction?.amount,
+ state: transaction?.invoice_state,
+ })),
excluded: props.excludedOrderIds,
dateFrom: props.dates?.dateFrom,
dateTo: props.dates?.dateTo,
- autoExpandAll: props.autoExpandAll,
})
);
+const snapshotIdentity = computed(() =>
+ JSON.stringify({
+ customerNumber: toPositiveInteger(props.customer?.customer_number),
+ dateFrom: props.dates?.dateFrom,
+ dateTo: props.dates?.dateTo,
+ })
+);
+const currentSignature = computed(() => `${snapshotIdentity.value}:${treeDataVersion.value}:${props.autoExpandAll}`);
const rememberNodes = (nodes: TreeNode[]) => {
const next = { ...nodeById.value };
@@ -163,6 +195,109 @@ const rememberNodes = (nodes: TreeNode[]) => {
};
const rootNodes = ref([]);
+const snapshotTreeLabels = () => ({
+ collection: nodeLabel.collection,
+ ordersWithoutCollection: nodeLabel.ordersWithoutCollection(),
+ orders: treeText("categories.orders", "Orders"),
+ items: treeText("categories.order_items", "Ordrelinjer"),
+ attachments: {
+ certificates: treeText("categories.certificates", "Vaskecertifikater"),
+ images: treeText("categories.images", "Billeder"),
+ other: treeText("categories.other_attachments", "Andre vedhæftninger"),
+ },
+ bookings: treeText("categories.bookings", "Bookinger"),
+ booking: nodeLabel.booking,
+ xlvask: nodeLabel.xlvaskEmpty(),
+ economic: treeText("categories.economic", "E-conomic"),
+ agreement: (id: any) => treeText("nodes.agreement", id ? `Aftale #${id}` : "Aftaler", { id }),
+ payment: (id: any) => treeText("nodes.payment", id ? `Betaling #${id}` : "Betalinger", { id }),
+ order: nodeLabel.order,
+ orderItemFallback: nodeLabel.orderItemFallback,
+ attachmentFallback: nodeLabel.attachmentFallback,
+ bookingItemFallback: nodeLabel.bookingItemFallback,
+ xlvaskItemFallback: nodeLabel.xlvaskItemFallback,
+});
+
+const buildLegacyRootNodes = () =>
+ buildCollectionRootNodes(props.customer, props.transactions, props.excludedOrderIds, {
+ collection: nodeLabel.collection,
+ ordersWithoutCollection: nodeLabel.ordersWithoutCollection(),
+ dateFrom: props.dates?.dateFrom,
+ dateTo: props.dates?.dateTo,
+ locale: localeValue(),
+ });
+
+const replaceRootNodes = (
+ nodes: TreeNode[],
+ { preserveState = false, consumedKeys = [] as any[] }: { preserveState?: boolean; consumedKeys?: any[] } = {}
+) => {
+ const previousExpanded = preserveState ? [...expandedKeys.value] : [];
+ const previousChecked = preserveState ? [...checkedKeys.value] : [];
+ const consumed = new Set(consumedKeys.map(String));
+ nodeById.value = {};
+ rootNodes.value = rememberNodes(nodes);
+ const validKeys = new Set(Object.keys(nodeById.value));
+ expandedKeys.value = previousExpanded.filter((key) => validKeys.has(String(key)));
+ checkedKeys.value = previousChecked.filter((key) => validKeys.has(String(key)) && !consumed.has(String(key)));
+ treeDataVersion.value += 1;
+};
+
+const customerAdvertisesSnapshot = () => {
+ const capabilities = props.capabilities ?? props.customer?.capabilities ?? props.customer?.invoicing_period_capabilities;
+ return capabilities?.object_tree_v2 === true;
+};
+
+const refreshSnapshot = async ({ force = false, consumedKeys = [] as any[] } = {}) => {
+ const requestGeneration = ++snapshotRequestGeneration;
+ const customerNumber = toPositiveInteger(props.customer?.customer_number);
+ if (!customerNumber || !props.dates?.dateFrom || !props.dates?.dateTo || !customerAdvertisesSnapshot()) {
+ snapshotLoading.value = false;
+ snapshotFallback.value = true;
+ legacySnapshotFallbackAllowed.value = false;
+ return null;
+ }
+ const requestedIdentity = snapshotIdentity.value;
+ snapshotLoading.value = true;
+ legacySnapshotFallbackAllowed.value = false;
+ try {
+ const snapshot = await snapshotLoader.load(
+ { customerNumber, dateFrom: props.dates.dateFrom, dateTo: props.dates.dateTo },
+ { force }
+ );
+ if (requestGeneration !== snapshotRequestGeneration || requestedIdentity !== snapshotIdentity.value) {
+ return null;
+ }
+ if (
+ !snapshot ||
+ snapshot.customer_number !== customerNumber ||
+ snapshot.date_from !== props.dates.dateFrom ||
+ snapshot.date_to !== props.dates.dateTo
+ ) {
+ snapshotFallback.value = true;
+ legacySnapshotFallbackAllowed.value = false;
+ return null;
+ }
+ activeSnapshot.value = snapshot;
+ snapshotFallback.value = false;
+ legacySnapshotFallbackAllowed.value = false;
+ replaceRootNodes(buildCompleteSnapshotRootNodes(snapshot, snapshotTreeLabels()), {
+ preserveState: true,
+ consumedKeys,
+ });
+ return snapshot;
+ } catch (error: any) {
+ if (requestGeneration === snapshotRequestGeneration && requestedIdentity === snapshotIdentity.value) {
+ const endpointUnavailable = isTreeEndpointUnavailable(error);
+ snapshotFallback.value = !endpointUnavailable;
+ legacySnapshotFallbackAllowed.value = endpointUnavailable;
+ }
+ return null;
+ } finally {
+ if (requestGeneration === snapshotRequestGeneration && requestedIdentity === snapshotIdentity.value) {
+ snapshotLoading.value = false;
+ }
+ }
+};
const expandableNodeKeys = computed(() =>
Object.values(nodeById.value)
.filter((node) => node?.isLeaf !== true && node?.disabled !== true && node?.id !== undefined && node?.id !== null)
@@ -190,31 +325,60 @@ const toggleExpandAll = async () => {
};
watch(
- currentSignature,
+ snapshotIdentity,
() => {
checkedKeys.value = [];
expandedKeys.value = [];
- nodeById.value = {};
nodeErrors.value = {};
orderItemRowsByOrderId.value = {};
attachmentRowsByOrderId.value = {};
activeTreeActionKey.value = null;
openActionGroupType.value = null;
+ activeSnapshot.value = null;
+ snapshotFallback.value = false;
+ legacySnapshotFallbackAllowed.value = false;
resetPreviewState();
-
- rootNodes.value = rememberNodes(
- buildCollectionRootNodes(props.customer, props.transactions, props.excludedOrderIds, {
- collection: nodeLabel.collection,
- ordersWithoutCollection: nodeLabel.ordersWithoutCollection(),
- dateFrom: props.dates?.dateFrom,
- dateTo: props.dates?.dateTo,
- locale: localeValue(),
- })
- );
+ replaceRootNodes(buildLegacyRootNodes());
+ void refreshSnapshot();
},
{ immediate: true }
);
+watch(legacyDataSignature, () => {
+ if (!activeSnapshot.value) {
+ replaceRootNodes(buildLegacyRootNodes());
+ }
+});
+
+watch(
+ () => customerAdvertisesSnapshot(),
+ (enabled) => {
+ if (!enabled) {
+ snapshotRequestGeneration += 1;
+ snapshotLoader.dispose();
+ snapshotLoading.value = false;
+ activeSnapshot.value = null;
+ snapshotFallback.value = false;
+ legacySnapshotFallbackAllowed.value = false;
+ replaceRootNodes(buildLegacyRootNodes(), { preserveState: true });
+ return;
+ }
+ void refreshSnapshot({ force: true });
+ }
+);
+
+watch(
+ () => localeValue(),
+ () => {
+ replaceRootNodes(
+ activeSnapshot.value
+ ? buildCompleteSnapshotRootNodes(activeSnapshot.value, snapshotTreeLabels())
+ : buildLegacyRootNodes(),
+ { preserveState: true }
+ );
+ }
+);
+
const selectedNodes = computed(() =>
checkedKeys.value.map((key) => nodeById.value[String(key)]).filter((node) => node?.actionable === true)
);
@@ -300,6 +464,7 @@ const certificateAttachmentOrderIds = computed(() => certificateAttachmentOrderI
const actionableXlvaskNodes = computed(() => actionableXlvaskNodesFromNodes(selectedNodes.value));
onBeforeUnmount(() => {
+ snapshotLoader.dispose();
resetPreviewState();
});
@@ -1066,6 +1231,20 @@ const getNodeIconColorClass = (node: TreeNode) =>
}[getNodeInvoiceState(node)]);
const getNodeSubtitle = (node: TreeNode) => {
if (node.type === TREE_NODE_TYPES.COLLECTION) {
+ if (activeSnapshot.value && node.meta.completeOrderCount !== undefined) {
+ return [
+ treeText(
+ "subtitles.selected_period_summary",
+ `Periode: ${node.meta.periodOrderCount} orders · ${formatCurrency(node.meta.periodTotalNetAmount)}`,
+ { count: node.meta.periodOrderCount, amount: formatCurrency(node.meta.periodTotalNetAmount) }
+ ),
+ treeText(
+ "subtitles.complete_collection_summary",
+ `Hele samlingen: ${node.meta.completeOrderCount} orders · ${formatCurrency(node.meta.completeTotalNetAmount)}`,
+ { count: node.meta.completeOrderCount, amount: formatCurrency(node.meta.completeTotalNetAmount) }
+ ),
+ ].join(" · ");
+ }
return `${node.meta.orderCount} orders · ${formatCurrency(node.meta.totalNetAmount)}`;
}
if (node.type === TREE_NODE_TYPES.ORDER) {
@@ -1381,7 +1560,7 @@ const downloadEconomicInvoiceNode = async (node: TreeNode) => {
economicType
);
openUrlInNewTab(getApiPayload(response)?.url);
- } catch (error) {
+ } catch (error: any) {
await Swal.fire({
title: treeText("errors.download_failed", "Download mislykkedes"),
text: SessionUser.functions.parseErrorMessage?.(error) || String(error),
@@ -1871,7 +2050,7 @@ const runActionWithPreview = async (
showConfirmButton: false,
});
emit("refresh");
- } catch (error) {
+ } catch (error: any) {
await Swal.fire({
title: treeText("errors.action_failed", "Handlingen mislykkedes"),
text: SessionUser.functions.parseErrorMessage?.(error) || String(error),
@@ -1913,44 +2092,11 @@ const normalizeInvoiceCollectionId = (value: any) => {
};
const renderBulkActionPreviewHtml = (preview: any) => {
- const summary = preview?.summary || {};
- const blockers = Array.isArray(preview?.blockers) ? preview.blockers : [];
- const examples = Array.isArray(preview?.examples) ? preview.examples : [];
- const lines = [
- t("invoicing_period.invoice_collection_actions.preview.collections", {
- count: summary.collection_count ?? collectionIds().length,
- }),
- t("invoicing_period.invoice_collection_actions.preview.changed", {
- count: summary.changed_count ?? 0,
- }),
- t("invoicing_period.invoice_collection_actions.preview.skipped", {
- count: summary.skipped_count ?? 0,
- }),
- ];
-
- if (preview?.options?.target_invoice_collection_id) {
- lines.push(
- t("invoicing_period.invoice_collection_actions.preview.merge_target", {
- id: preview.options.target_invoice_collection_id,
- })
- );
- }
-
- if (examples.length > 0) {
- lines.push(t("invoicing_period.invoice_collection_actions.preview.affected_examples"));
- examples.slice(0, 8).forEach((example: any) => {
- lines.push(example.message || JSON.stringify(example));
- });
- }
-
- if (blockers.length > 0) {
- lines.push(t("invoicing_period.invoice_collection_actions.preview.blockers"));
- blockers.forEach((blocker: any) => {
- lines.push(blocker.message || blocker.code);
- });
- }
-
- return `${lines.map((line) => `
${escapeHtml(line)}
`).join("")}
`;
+ return renderInvoiceCollectionActionPreviewHtml({
+ preview: normalizeInvoiceCollectionActionPreview(preview, collectionIds().length),
+ t,
+ formatCurrency,
+ });
};
const chooseMergeTargetInvoiceCollection = async (invoiceCollectionIds: number[]) => {
@@ -2005,15 +2151,43 @@ const runCollectionBulkAction = async (
}
try {
- const previewResponse = await SessionUser.objects.collectedOrderInvoices.functions.bulk_action_preview(
- action,
- ids,
- options,
- localeValue()
- );
- const preview = getApiPayload(previewResponse);
- const blockers = Array.isArray(preview.blockers) ? preview.blockers : [];
- const changedCount = Number(preview?.summary?.changed_count ?? 0);
+ const canUseTreeAction =
+ (activeSnapshot.value?.capabilities.actions as Record | undefined)?.[action] === true;
+ let useTreeAction = canUseTreeAction;
+ let previewResponse: any;
+ if (useTreeAction) {
+ try {
+ previewResponse = await SessionUser.objects.collectedOrderInvoices.functions.period_tree_action_preview({
+ action,
+ invoice_collection_ids: ids,
+ options,
+ customer_number: activeSnapshot.value?.customer_number,
+ snapshot_revision: activeSnapshot.value?.snapshot_revision,
+ locale: localeValue(),
+ });
+ } catch (error) {
+ if (!isInvoiceCollectionTreeActionUnavailable(error)) {
+ throw error;
+ }
+ useTreeAction = false;
+ }
+ }
+ if (!useTreeAction) {
+ previewResponse = await SessionUser.objects.collectedOrderInvoices.functions.bulk_action_preview(
+ action,
+ ids,
+ options,
+ localeValue(),
+ {
+ customer_number: activeSnapshot.value?.customer_number,
+ snapshot_revision: activeSnapshot.value?.snapshot_revision,
+ }
+ );
+ }
+ const previewRaw = getApiPayload(previewResponse);
+ const preview = normalizeInvoiceCollectionActionPreview(previewRaw, ids.length);
+ const blockers = preview.blockers;
+ const changedCount = preview.changedCount;
if (blockers.length > 0 || changedCount === 0) {
await Swal.fire({
@@ -2022,7 +2196,7 @@ const runCollectionBulkAction = async (
blockers.length > 0
? t("invoicing_period.invoice_collection_actions.preview.blocked_title")
: t("invoicing_period.invoice_collection_actions.preview.no_changes_title"),
- html: renderBulkActionPreviewHtml(preview),
+ html: renderBulkActionPreviewHtml(previewRaw),
});
return;
}
@@ -2032,19 +2206,19 @@ const runCollectionBulkAction = async (
title: t("invoicing_period.invoice_collection_actions.preview.title", {
action: getBulkActionLabel(action),
}),
- html: renderBulkActionPreviewHtml(preview),
+ html: renderBulkActionPreviewHtml(previewRaw),
input: "text",
inputLabel: t("invoicing_period.invoice_collection_actions.preview.confirmation_label", {
- phrase: preview.confirmation_phrase,
+ phrase: preview.confirmationPhrase,
}),
- inputPlaceholder: preview.confirmation_phrase,
+ inputPlaceholder: preview.confirmationPhrase,
showCancelButton: true,
confirmButtonText: t("invoicing_period.invoice_collection_actions.preview.confirm_button"),
cancelButtonText: commonText("cancel", "Annuller"),
inputValidator: (value) => {
- if (String(value ?? "").trim() !== String(preview.confirmation_phrase ?? "")) {
+ if (String(value ?? "").trim() !== String(preview.confirmationPhrase ?? "")) {
return t("invoicing_period.invoice_collection_actions.preview.confirmation_mismatch", {
- phrase: preview.confirmation_phrase,
+ phrase: preview.confirmationPhrase,
});
}
return undefined;
@@ -2055,14 +2229,32 @@ const runCollectionBulkAction = async (
return;
}
- const applyResponse = await SessionUser.objects.collectedOrderInvoices.functions.bulk_action_apply({
- preview_id: preview.preview_id,
- action,
- invoice_collection_ids: ids,
- options,
- confirmation_text: confirmation.value,
- locale: localeValue(),
- });
+ let applyResponse: any;
+ if (useTreeAction) {
+ try {
+ applyResponse = await SessionUser.objects.collectedOrderInvoices.functions.period_tree_action_apply({
+ preview_id: preview.previewId,
+ confirmation_text: confirmation.value,
+ });
+ } catch (error) {
+ if (!isInvoiceCollectionTreeActionUnavailable(error)) {
+ throw error;
+ }
+ useTreeAction = false;
+ }
+ }
+ if (!useTreeAction) {
+ applyResponse = await SessionUser.objects.collectedOrderInvoices.functions.bulk_action_apply({
+ preview_id: preview.previewId,
+ action,
+ invoice_collection_ids: ids,
+ options,
+ confirmation_text: confirmation.value,
+ locale: localeValue(),
+ customer_number: activeSnapshot.value?.customer_number,
+ snapshot_revision: activeSnapshot.value?.snapshot_revision,
+ });
+ }
const applied = getApiPayload(applyResponse);
if (action === INVOICE_COLLECTION_BULK_ACTIONS.QUEUE_ECONOMIC) {
@@ -2079,7 +2271,26 @@ const runCollectionBulkAction = async (
invoiceQueue.processInvoiceCollectionQueue();
}
}
- if (runOptions.clearSelection !== false) {
+ const actedCollectionIds = new Set(ids.map(Number));
+ const consumedNodeKeys = new Set();
+ const collectConsumedNodeKeys = (node: TreeNode, withinActedCollection = false) => {
+ const isActedCollection =
+ withinActedCollection ||
+ (node.type === TREE_NODE_TYPES.COLLECTION && actedCollectionIds.has(Number(node.meta?.collectionId)));
+ if (isActedCollection) {
+ consumedNodeKeys.add(String(node.id));
+ }
+ (node.children || []).forEach((child: TreeNode) => collectConsumedNodeKeys(child, isActedCollection));
+ };
+ rootNodes.value.forEach((node) => collectConsumedNodeKeys(node));
+ const consumedKeys = checkedKeys.value.filter((key) => consumedNodeKeys.has(String(key)));
+ if (activeSnapshot.value) {
+ const refreshedSnapshot = await refreshSnapshot({ force: true, consumedKeys });
+ if (!refreshedSnapshot) {
+ const consumed = new Set(consumedKeys.map(String));
+ checkedKeys.value = checkedKeys.value.filter((key) => !consumed.has(String(key)));
+ }
+ } else if (runOptions.clearSelection !== false) {
checkedKeys.value = [];
}
await Swal.fire({
@@ -2092,7 +2303,10 @@ const runCollectionBulkAction = async (
showConfirmButton: false,
});
emit("refresh");
- } catch (error) {
+ } catch (error: any) {
+ if (activeSnapshot.value && Number(error?.response?.status) === 409) {
+ await refreshSnapshot({ force: true });
+ }
await Swal.fire({
icon: "error",
title: t("invoicing_period.invoice_collection_actions.error_title"),
@@ -2420,45 +2634,53 @@ const decideXlvaskAutomation = (
const decideSelectedXlvaskAutomation = (decision: "accept" | "deny") =>
decideXlvaskAutomation(decision, actionableXlvaskNodes.value);
+const isCollectionActionAvailable = (action: string) =>
+ (!activeSnapshot.value && !customerAdvertisesSnapshot()) ||
+ (!activeSnapshot.value && legacySnapshotFallbackAllowed.value && !snapshotLoading.value) ||
+ (!!activeSnapshot.value && !snapshotFallback.value &&
+ (activeSnapshot.value.capabilities.actions as Record | undefined)?.[action] === true);
+const availableCollectionAction = (capability: string, action: TreeAction): TreeAction | null =>
+ isCollectionActionAvailable(capability) ? action : null;
+
const collectionActions = (): TreeAction[] => [
- {
+ availableCollectionAction(INVOICE_COLLECTION_BULK_ACTIONS.QUEUE_ECONOMIC, {
key: "collection:queue-economic",
label: t("invoicing_period.invoice_collection_actions.actions.queue_economic"),
icon: "fa-file-invoice",
tone: "dark",
affectedCount: selectedCollectionCount.value,
run: () => runCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.QUEUE_ECONOMIC),
- },
- {
+ }),
+ availableCollectionAction(INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES, {
key: "collection:clean-customer-rules",
label: t("invoicing_period.invoice_collection_actions.actions.remove_customer_rule_violations"),
icon: "fa-broom",
affectedCount: selectedCollectionCount.value,
run: () => runCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES),
- },
- {
+ }),
+ availableCollectionAction(INVOICE_COLLECTION_BULK_ACTIONS.MERGE, {
key: "collection:merge",
label: t("invoicing_period.invoice_collection_actions.actions.merge_collections"),
icon: "fa-compress-arrows-alt",
affectedCount: selectedCollectionCount.value > 1 ? selectedCollectionCount.value : 0,
disabled: selectedCollectionCount.value < 2,
run: () => runCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.MERGE),
- },
- {
+ }),
+ availableCollectionAction(INVOICE_COLLECTION_BULK_ACTIONS.SPLIT_BY_MONTH, {
key: "collection:split-by-month",
label: t("invoicing_period.invoice_collection_actions.actions.split_by_month"),
icon: "fa-calendar-alt",
affectedCount: selectedCollectionCount.value,
run: () => runCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.SPLIT_BY_MONTH),
- },
- {
+ }),
+ availableCollectionAction(INVOICE_COLLECTION_BULK_ACTIONS.RESET_HIDDEN_PRICES, {
key: "collection:reset-hidden-prices",
label: t("invoicing_period.invoice_collection_actions.actions.reset_hidden_item_prices"),
icon: "fa-tags",
affectedCount: selectedCollectionCount.value,
run: () => runCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.RESET_HIDDEN_PRICES),
- },
-];
+ }),
+].filter(Boolean) as TreeAction[];
const orderActions = (): TreeAction[] => [
{
@@ -2716,15 +2938,15 @@ const collectionNodeActions = (node: TreeNode): TreeAction[] => {
}
return [
- rowAction(node, "collection:queue-economic", {
+ isCollectionActionAvailable(INVOICE_COLLECTION_BULK_ACTIONS.QUEUE_ECONOMIC) ? rowAction(node, "collection:queue-economic", {
label: t("invoicing_period.invoice_collection_actions.actions.queue_economic"),
icon: "fa-file-invoice",
tone: "dark",
affectedCount: 1,
run: () =>
runCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.QUEUE_ECONOMIC, [collectionId], rowActionOptions()),
- }),
- rowAction(node, "collection:clean-customer-rules", {
+ }) : null,
+ isCollectionActionAvailable(INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES) ? rowAction(node, "collection:clean-customer-rules", {
label: t("invoicing_period.invoice_collection_actions.actions.remove_customer_rule_violations"),
icon: "fa-broom",
affectedCount: 1,
@@ -2734,15 +2956,15 @@ const collectionNodeActions = (node: TreeNode): TreeAction[] => {
[collectionId],
rowActionOptions()
),
- }),
- rowAction(node, "collection:split-by-month", {
+ }) : null,
+ isCollectionActionAvailable(INVOICE_COLLECTION_BULK_ACTIONS.SPLIT_BY_MONTH) ? rowAction(node, "collection:split-by-month", {
label: t("invoicing_period.invoice_collection_actions.actions.split_by_month"),
icon: "fa-calendar-alt",
affectedCount: 1,
run: () =>
runCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.SPLIT_BY_MONTH, [collectionId], rowActionOptions()),
- }),
- rowAction(node, "collection:reset-hidden-prices", {
+ }) : null,
+ isCollectionActionAvailable(INVOICE_COLLECTION_BULK_ACTIONS.RESET_HIDDEN_PRICES) ? rowAction(node, "collection:reset-hidden-prices", {
label: t("invoicing_period.invoice_collection_actions.actions.reset_hidden_item_prices"),
icon: "fa-tags",
affectedCount: 1,
@@ -2752,8 +2974,8 @@ const collectionNodeActions = (node: TreeNode): TreeAction[] => {
[collectionId],
rowActionOptions()
),
- }),
- ];
+ }) : null,
+ ].filter(Boolean) as TreeAction[];
};
const orderNodeActions = (node: TreeNode): TreeAction[] => {
@@ -3012,6 +3234,10 @@ const nodeActionWheelProps = (node: TreeNode) => {
}}
+
+
+ {{ treeText("preview.loading", "Indlæser forhåndsvisning") }}
+
{{ selectedCountLabel }}