Align invoicing period review workspace (#251)
Keep review navigation, customer cards, metadata, date labels, and direct order-item tables aligned across desktop and responsive layouts.
This commit is contained in:
@@ -96,6 +96,7 @@ onBeforeUnmount(disconnectLoadMoreObserver);
|
|||||||
>
|
>
|
||||||
<div class="b-tree-node-content" @click.stop="tree.handleNodeClick(node)">
|
<div class="b-tree-node-content" @click.stop="tree.handleNodeClick(node)">
|
||||||
<button
|
<button
|
||||||
|
v-if="node.hideToggle !== true"
|
||||||
type="button"
|
type="button"
|
||||||
class="button is-white is-small b-tree-node-toggle"
|
class="button is-white is-small b-tree-node-toggle"
|
||||||
:class="{ 'is-invisible': !hasExpandToggle }"
|
:class="{ 'is-invisible': !hasExpandToggle }"
|
||||||
@@ -122,7 +123,7 @@ onBeforeUnmount(disconnectLoadMoreObserver);
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span class="b-tree-node-icon">
|
<span v-if="node.hideIcon !== true" class="b-tree-node-icon">
|
||||||
<slot name="icon" :node="node" :expanded="isExpanded" :loading="isLoading" :error="hasLoadError">
|
<slot name="icon" :node="node" :expanded="isExpanded" :loading="isLoading" :error="hasLoadError">
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<i class="fas" :class="hasExpandToggle ? (isExpanded ? 'fa-folder-open' : 'fa-folder') : 'fa-file'"></i>
|
<i class="fas" :class="hasExpandToggle ? (isExpanded ? 'fa-folder-open' : 'fa-folder') : 'fa-file'"></i>
|
||||||
|
|||||||
+24
-11
@@ -437,7 +437,7 @@ const saveFlagStatus = async (flag: any, status: string) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.invoice-period-flags {
|
.invoice-period-flags {
|
||||||
margin: -0.35rem 3.25rem 0.65rem 1.65rem;
|
margin: -0.15rem 0.6rem 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-period-flags--compact {
|
.invoice-period-flags--compact {
|
||||||
@@ -445,31 +445,44 @@ const saveFlagStatus = async (flag: any, status: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.invoice-period-flag-row {
|
.invoice-period-flag-row {
|
||||||
display: grid;
|
align-items: start;
|
||||||
grid-template-columns: 1.2rem minmax(0, 1fr) auto;
|
|
||||||
gap: 0.45rem;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 1.75rem;
|
|
||||||
padding: 0.15rem 0.35rem;
|
|
||||||
margin-top: 0.35rem;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
column-gap: 0.45rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.2rem minmax(0, 1fr);
|
||||||
|
min-height: 1.75rem;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
padding: 0.15rem 0.35rem;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-period-flag-row__message {
|
.invoice-period-flag-row__message {
|
||||||
min-width: 0;
|
|
||||||
color: #4a4a4a;
|
color: #4a4a4a;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
|
min-width: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-period-flag-row__actions {
|
.invoice-period-flag-row__actions {
|
||||||
|
grid-column: 2;
|
||||||
|
justify-self: end;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
max-height: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 120ms ease;
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: max-height 120ms ease, opacity 120ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-period-flag-row:hover .invoice-period-flag-row__actions {
|
.invoice-period-flag-row:hover .invoice-period-flag-row__actions,
|
||||||
|
.invoice-period-flag-row:focus-within .invoice-period-flag-row__actions {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
max-height: 2.5rem;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-period-flag-row__tooltip {
|
.invoice-period-flag-row__tooltip {
|
||||||
|
|||||||
+357
-84
@@ -8,6 +8,7 @@ import EditableTableColumn from "@/components/displays/buttons/EditableTableColu
|
|||||||
import InvoicingPeriodFlagBadge from "./InvoicingPeriodFlagBadge.vue";
|
import InvoicingPeriodFlagBadge from "./InvoicingPeriodFlagBadge.vue";
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { getAttachmentPreviewKind, releaseObjectUrl } from "@/services/attachmentPreview.js";
|
import { getAttachmentPreviewKind, releaseObjectUrl } from "@/services/attachmentPreview.js";
|
||||||
|
import { getDepartmentName } from "@/components/pagination/departmentTabs.vue";
|
||||||
import { invoiceQueue } from "../imports/InvoicingBillingPeriodImportInvoiceQueue.vue";
|
import { invoiceQueue } from "../imports/InvoicingBillingPeriodImportInvoiceQueue.vue";
|
||||||
import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
|
import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +26,7 @@ import {
|
|||||||
TREE_CATEGORY_TYPES,
|
TREE_CATEGORY_TYPES,
|
||||||
TREE_NODE_TYPES,
|
TREE_NODE_TYPES,
|
||||||
buildCollectionRootNodes,
|
buildCollectionRootNodes,
|
||||||
buildOrderItemTree,
|
buildOrderItemTableRows,
|
||||||
classifyAttachment,
|
classifyAttachment,
|
||||||
hasWashCertificateOrderItem,
|
hasWashCertificateOrderItem,
|
||||||
makeAttachmentNode,
|
makeAttachmentNode,
|
||||||
@@ -127,6 +128,18 @@ const treeText = (key: string, fallback: string, params: Record<string, any> = {
|
|||||||
tr(`invoicing_period.object_tree.${key}`, fallback, params);
|
tr(`invoicing_period.object_tree.${key}`, fallback, params);
|
||||||
const commonText = (key: string, fallback: string, params: Record<string, any> = {}) =>
|
const commonText = (key: string, fallback: string, params: Record<string, any> = {}) =>
|
||||||
tr(`common.${key}`, fallback, params);
|
tr(`common.${key}`, fallback, params);
|
||||||
|
const orderDepartmentLabel = (order: any) => {
|
||||||
|
const explicitName = order?.department_name || order?.department?.name;
|
||||||
|
if (explicitName) {
|
||||||
|
return explicitName;
|
||||||
|
}
|
||||||
|
const departmentId = toPositiveInteger(order?.department_id);
|
||||||
|
if (!departmentId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const resolvedName = getDepartmentName(departmentId);
|
||||||
|
return resolvedName && String(resolvedName) !== String(departmentId) ? resolvedName : `#${departmentId}`;
|
||||||
|
};
|
||||||
const loadFailedMessage = () => treeText("errors.load_failed", "Kunne ikke indlæse indholdet.");
|
const loadFailedMessage = () => treeText("errors.load_failed", "Kunne ikke indlæse indholdet.");
|
||||||
const openUrlInNewTab = (url: string | null | undefined) => {
|
const openUrlInNewTab = (url: string | null | undefined) => {
|
||||||
if (!url || typeof window === "undefined") {
|
if (!url || typeof window === "undefined") {
|
||||||
@@ -164,6 +177,11 @@ const legacyDataSignature = computed(() =>
|
|||||||
transactions: props.transactions.map((transaction) => ({
|
transactions: props.transactions.map((transaction) => ({
|
||||||
id: transaction?.id,
|
id: transaction?.id,
|
||||||
collectionId: transaction?.invoice_collection_id,
|
collectionId: transaction?.invoice_collection_id,
|
||||||
|
date:
|
||||||
|
transaction?.date ??
|
||||||
|
transaction?.created_at ??
|
||||||
|
transaction?.closed_at ??
|
||||||
|
transaction?.updated_at,
|
||||||
amount: transaction?.total_net_amount ?? transaction?.amount,
|
amount: transaction?.total_net_amount ?? transaction?.amount,
|
||||||
state: transaction?.invoice_state,
|
state: transaction?.invoice_state,
|
||||||
})),
|
})),
|
||||||
@@ -195,8 +213,35 @@ const rememberNodes = (nodes: TreeNode[]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rootNodes = ref<TreeNode[]>([]);
|
const rootNodes = ref<TreeNode[]>([]);
|
||||||
|
|
||||||
|
const orderItemNodeKeys = (orderId: any) => {
|
||||||
|
const normalizedOrderId = toPositiveInteger(orderId);
|
||||||
|
return Object.values(nodeById.value)
|
||||||
|
.filter(
|
||||||
|
(node) =>
|
||||||
|
node.type === TREE_NODE_TYPES.ORDER_ITEM && toPositiveInteger(node.meta?.orderId) === normalizedOrderId
|
||||||
|
)
|
||||||
|
.map((node) => node.id);
|
||||||
|
};
|
||||||
|
const areAllOrderItemsChecked = (orderId: any) => {
|
||||||
|
const keys = orderItemNodeKeys(orderId);
|
||||||
|
return keys.length > 0 && keys.every((key) => checkedKeys.value.includes(key));
|
||||||
|
};
|
||||||
|
const areSomeOrderItemsChecked = (orderId: any) => {
|
||||||
|
const keys = orderItemNodeKeys(orderId);
|
||||||
|
const selectedCount = keys.filter((key) => checkedKeys.value.includes(key)).length;
|
||||||
|
return selectedCount > 0 && selectedCount < keys.length;
|
||||||
|
};
|
||||||
|
const toggleOrderItemRows = (orderId: any) => {
|
||||||
|
const keys = orderItemNodeKeys(orderId);
|
||||||
|
const keySet = new Set(keys);
|
||||||
|
checkedKeys.value = areAllOrderItemsChecked(orderId)
|
||||||
|
? checkedKeys.value.filter((key) => !keySet.has(key))
|
||||||
|
: [...new Set([...checkedKeys.value, ...keys])];
|
||||||
|
};
|
||||||
const snapshotTreeLabels = () => ({
|
const snapshotTreeLabels = () => ({
|
||||||
collection: nodeLabel.collection,
|
collection: nodeLabel.collection,
|
||||||
|
locale: localeValue(),
|
||||||
ordersWithoutCollection: nodeLabel.ordersWithoutCollection(),
|
ordersWithoutCollection: nodeLabel.ordersWithoutCollection(),
|
||||||
orders: treeText("categories.orders", "Orders"),
|
orders: treeText("categories.orders", "Orders"),
|
||||||
items: treeText("categories.order_items", "Ordrelinjer"),
|
items: treeText("categories.order_items", "Ordrelinjer"),
|
||||||
@@ -222,8 +267,6 @@ const buildLegacyRootNodes = () =>
|
|||||||
buildCollectionRootNodes(props.customer, props.transactions, props.excludedOrderIds, {
|
buildCollectionRootNodes(props.customer, props.transactions, props.excludedOrderIds, {
|
||||||
collection: nodeLabel.collection,
|
collection: nodeLabel.collection,
|
||||||
ordersWithoutCollection: nodeLabel.ordersWithoutCollection(),
|
ordersWithoutCollection: nodeLabel.ordersWithoutCollection(),
|
||||||
dateFrom: props.dates?.dateFrom,
|
|
||||||
dateTo: props.dates?.dateTo,
|
|
||||||
locale: localeValue(),
|
locale: localeValue(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -490,10 +533,6 @@ const loadTreeNodeChildren = async (node: TreeNode) => {
|
|||||||
if (requestedSignature !== currentSignature.value) {
|
if (requestedSignature !== currentSignature.value) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
await preloadDefaultExpandedChildren(node, children);
|
|
||||||
if (requestedSignature !== currentSignature.value) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return rememberNodes(children);
|
return rememberNodes(children);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (requestedSignature !== currentSignature.value) {
|
if (requestedSignature !== currentSignature.value) {
|
||||||
@@ -505,30 +544,6 @@ const loadTreeNodeChildren = async (node: TreeNode) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const expandTreeNodeKey = (key: string | null | undefined) => {
|
|
||||||
if (!key || expandedKeys.value.includes(key)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
expandedKeys.value = [...expandedKeys.value, key];
|
|
||||||
};
|
|
||||||
|
|
||||||
const preloadDefaultExpandedChildren = async (node: TreeNode, children: TreeNode[]) => {
|
|
||||||
if (node.type !== TREE_NODE_TYPES.ORDER) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderItemsCategory = children.find((child) => child.category === TREE_CATEGORY_TYPES.ORDER_ITEMS);
|
|
||||||
if (!orderItemsCategory) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(orderItemsCategory.children)) {
|
|
||||||
orderItemsCategory.children = await loadCategoryChildren(orderItemsCategory);
|
|
||||||
orderItemsCategory.isLeaf = orderItemsCategory.children.length === 0;
|
|
||||||
}
|
|
||||||
expandTreeNodeKey(orderItemsCategory.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadChildrenForNode = async (node: TreeNode): Promise<TreeNode[]> => {
|
const loadChildrenForNode = async (node: TreeNode): Promise<TreeNode[]> => {
|
||||||
if (node.type === TREE_NODE_TYPES.COLLECTION) {
|
if (node.type === TREE_NODE_TYPES.COLLECTION) {
|
||||||
return buildCollectionCategoryNodes(node);
|
return buildCollectionCategoryNodes(node);
|
||||||
@@ -709,10 +724,6 @@ const loadCategoryChildren = async (node: TreeNode): Promise<TreeNode[]> => {
|
|||||||
return buildEconomicInvoiceNodes(node);
|
return buildEconomicInvoiceNodes(node);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.category === TREE_CATEGORY_TYPES.ORDER_ITEMS) {
|
|
||||||
return loadOrderItems(node.meta.orderId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
node.category === TREE_CATEGORY_TYPES.ORDER_ATTACHMENTS_CERTIFICATES ||
|
node.category === TREE_CATEGORY_TYPES.ORDER_ATTACHMENTS_CERTIFICATES ||
|
||||||
node.category === TREE_CATEGORY_TYPES.ORDER_ATTACHMENTS_IMAGES ||
|
node.category === TREE_CATEGORY_TYPES.ORDER_ATTACHMENTS_IMAGES ||
|
||||||
@@ -945,18 +956,12 @@ const loadOrderCategoryNodes = async (orderNode: TreeNode) => {
|
|||||||
getCachedAttachmentRows(orderId, attachmentSeed),
|
getCachedAttachmentRows(orderId, attachmentSeed),
|
||||||
]);
|
]);
|
||||||
const attachmentGroups = groupAttachments(attachments);
|
const attachmentGroups = groupAttachments(attachments);
|
||||||
const categories: TreeNode[] = [
|
const itemNodes = buildOrderItemTableRows(orderItems, {
|
||||||
makeCategoryNode({
|
fallbackLabel: (item: any) => nodeLabel.orderItemFallback(item?.id ?? item?.order_item_id),
|
||||||
id: makeNodeId(TREE_NODE_TYPES.CATEGORY, orderId, TREE_CATEGORY_TYPES.ORDER_ITEMS),
|
invoiceState: getNodeInvoiceState(orderNode),
|
||||||
label: treeText("categories.order_items", "Order items"),
|
orderId,
|
||||||
category: TREE_CATEGORY_TYPES.ORDER_ITEMS,
|
});
|
||||||
parentType: orderNode.type,
|
const categories: TreeNode[] = [];
|
||||||
parentId: orderNode.id,
|
|
||||||
icon: "fa-list-check",
|
|
||||||
checkable: true,
|
|
||||||
meta: { orderId, items: orderItems, invoiceState: getNodeInvoiceState(orderNode) },
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
|
|
||||||
if (shouldShowWashCertificateCategory(orderId, orderItems, attachmentGroups)) {
|
if (shouldShowWashCertificateCategory(orderId, orderItems, attachmentGroups)) {
|
||||||
categories.push(
|
categories.push(
|
||||||
@@ -1016,7 +1021,7 @@ const loadOrderCategoryNodes = async (orderNode: TreeNode) => {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return categories;
|
return [...itemNodes, ...categories];
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadOrder = async (orderId: number | null, fallback: any = null) => {
|
const loadOrder = async (orderId: number | null, fallback: any = null) => {
|
||||||
@@ -1029,15 +1034,6 @@ const loadOrder = async (orderId: number | null, fallback: any = null) => {
|
|||||||
|
|
||||||
const nodeOrderId = (node: TreeNode) => toPositiveInteger(node?.meta?.orderId ?? node?.meta?.order?.id);
|
const nodeOrderId = (node: TreeNode) => toPositiveInteger(node?.meta?.orderId ?? node?.meta?.order?.id);
|
||||||
|
|
||||||
const loadOrderItems = async (orderId: number) => {
|
|
||||||
const rows = await getCachedOrderItemRows(orderId);
|
|
||||||
const orderNode = nodeById.value[makeNodeId(TREE_NODE_TYPES.ORDER, orderId)];
|
|
||||||
return buildOrderItemTree(rows, {
|
|
||||||
fallbackLabel: (item: any) => nodeLabel.orderItemFallback(item?.id ?? item?.order_item_id),
|
|
||||||
invoiceState: getNodeInvoiceState(orderNode),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const makeAttachmentCategory = (orderId: number, category: string, label: string, attachments: any[]) =>
|
const makeAttachmentCategory = (orderId: number, category: string, label: string, attachments: any[]) =>
|
||||||
makeCategoryNode({
|
makeCategoryNode({
|
||||||
id: makeNodeId(TREE_NODE_TYPES.CATEGORY, orderId, category),
|
id: makeNodeId(TREE_NODE_TYPES.CATEGORY, orderId, category),
|
||||||
@@ -1785,6 +1781,13 @@ const getNodeFields = (node: TreeNode): NodeField[] => {
|
|||||||
const object = collectionObjectForNode(node);
|
const object = collectionObjectForNode(node);
|
||||||
const columns = SessionUser.objects.collectedOrderInvoices.columns;
|
const columns = SessionUser.objects.collectedOrderInvoices.columns;
|
||||||
return [
|
return [
|
||||||
|
makeNodeField({
|
||||||
|
key: "state",
|
||||||
|
label: treeText("fields.collection_state", "Status"),
|
||||||
|
value: object.state,
|
||||||
|
editable: false,
|
||||||
|
slot: 1,
|
||||||
|
}),
|
||||||
makeNodeField({
|
makeNodeField({
|
||||||
key: "po_number",
|
key: "po_number",
|
||||||
label: fieldLabel(columns, "po_number", "PO"),
|
label: fieldLabel(columns, "po_number", "PO"),
|
||||||
@@ -1805,6 +1808,13 @@ const getNodeFields = (node: TreeNode): NodeField[] => {
|
|||||||
editFunction: SessionUser.objects.collectedOrderInvoices.showEditObjectFieldForm,
|
editFunction: SessionUser.objects.collectedOrderInvoices.showEditObjectFieldForm,
|
||||||
slot: 3,
|
slot: 3,
|
||||||
}),
|
}),
|
||||||
|
makeNodeField({
|
||||||
|
key: "booked_invoice_id",
|
||||||
|
label: treeText("fields.booked_invoice_id", "Bogført faktura-ID"),
|
||||||
|
value: object.booked_invoice_id,
|
||||||
|
editable: false,
|
||||||
|
slot: 4,
|
||||||
|
}),
|
||||||
makeNodeField({
|
makeNodeField({
|
||||||
key: "external_id",
|
key: "external_id",
|
||||||
label: fieldLabel(columns, "external_id", "Eksternt ID"),
|
label: fieldLabel(columns, "external_id", "Eksternt ID"),
|
||||||
@@ -1825,18 +1835,11 @@ const getNodeFields = (node: TreeNode): NodeField[] => {
|
|||||||
slot: 6,
|
slot: 6,
|
||||||
}),
|
}),
|
||||||
makeNodeField({
|
makeNodeField({
|
||||||
key: "state",
|
key: "processor",
|
||||||
label: treeText("fields.collection_state", "Status"),
|
label: treeText("fields.processor", "Behandler"),
|
||||||
value: object.state,
|
value: object.processor,
|
||||||
editable: false,
|
editable: false,
|
||||||
slot: 1,
|
slot: 7,
|
||||||
}),
|
|
||||||
makeNodeField({
|
|
||||||
key: "booked_invoice_id",
|
|
||||||
label: treeText("fields.booked_invoice_id", "Bogført faktura-ID"),
|
|
||||||
value: object.booked_invoice_id,
|
|
||||||
editable: false,
|
|
||||||
slot: 4,
|
|
||||||
}),
|
}),
|
||||||
makeNodeField({
|
makeNodeField({
|
||||||
key: "total_net_amount",
|
key: "total_net_amount",
|
||||||
@@ -1846,13 +1849,6 @@ const getNodeFields = (node: TreeNode): NodeField[] => {
|
|||||||
slot: 8,
|
slot: 8,
|
||||||
numeric: true,
|
numeric: true,
|
||||||
}),
|
}),
|
||||||
makeNodeField({
|
|
||||||
key: "processor",
|
|
||||||
label: treeText("fields.processor", "Behandler"),
|
|
||||||
value: object.processor,
|
|
||||||
editable: false,
|
|
||||||
slot: 7,
|
|
||||||
}),
|
|
||||||
makeNodeField({
|
makeNodeField({
|
||||||
key: "error_message",
|
key: "error_message",
|
||||||
label: treeText("fields.error_message", "Fejl"),
|
label: treeText("fields.error_message", "Fejl"),
|
||||||
@@ -1916,6 +1912,13 @@ const getNodeFields = (node: TreeNode): NodeField[] => {
|
|||||||
editFunction: SessionUser.objects.orders.showEditObjectFieldForm,
|
editFunction: SessionUser.objects.orders.showEditObjectFieldForm,
|
||||||
slot: 3,
|
slot: 3,
|
||||||
}),
|
}),
|
||||||
|
makeNodeField({
|
||||||
|
key: "department",
|
||||||
|
label: commonText("department", "Afdeling"),
|
||||||
|
value: orderDepartmentLabel(object),
|
||||||
|
editable: false,
|
||||||
|
slot: 7,
|
||||||
|
}),
|
||||||
registrationField(object, columns),
|
registrationField(object, columns),
|
||||||
makeNodeField({
|
makeNodeField({
|
||||||
key: "include_in_invoice",
|
key: "include_in_invoice",
|
||||||
@@ -3351,6 +3354,7 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="invoice-period-object-tree__scroll">
|
||||||
<BuefyTree
|
<BuefyTree
|
||||||
:key="currentSignature"
|
:key="currentSignature"
|
||||||
ref="treeRef"
|
ref="treeRef"
|
||||||
@@ -3388,7 +3392,101 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
@mouseleave="hideNodePreview(node)"
|
@mouseleave="hideNodePreview(node)"
|
||||||
@focusout="hideNodePreview(node)"
|
@focusout="hideNodePreview(node)"
|
||||||
>
|
>
|
||||||
<div class="invoice-period-tree-node__row">
|
<div
|
||||||
|
v-if="node.type === TREE_NODE_TYPES.ORDER_ITEM"
|
||||||
|
class="invoice-period-order-item-table"
|
||||||
|
:data-testid="`invoice-period-order-item-table-row-${node.meta.itemId}`"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="node.meta.showTableHeader"
|
||||||
|
class="invoice-period-order-item-table__header"
|
||||||
|
:data-testid="`invoice-period-order-item-table-header-${node.meta.orderId}`"
|
||||||
|
>
|
||||||
|
<label class="invoice-period-order-item-table__select-all" @click.stop>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="areAllOrderItemsChecked(node.meta.orderId)"
|
||||||
|
:indeterminate="areSomeOrderItemsChecked(node.meta.orderId)"
|
||||||
|
:aria-label="treeText('order_items.select_all', 'Vælg alle ordrelinjer')"
|
||||||
|
:data-testid="`invoice-period-order-item-table-select-all-${node.meta.orderId}`"
|
||||||
|
@change.stop="toggleOrderItemRows(node.meta.orderId)"
|
||||||
|
/>
|
||||||
|
<span aria-hidden="true">{{ commonText("product", "Produkt") }}</span>
|
||||||
|
</label>
|
||||||
|
<span v-for="field in visibleNodeFields(node)" :key="field.key" aria-hidden="true">
|
||||||
|
{{ field.label }}
|
||||||
|
</span>
|
||||||
|
<span class="invoice-period-order-item-table__actions-header" aria-hidden="true">
|
||||||
|
{{ commonText("actions", "Handlinger") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="invoice-period-order-item-table__row">
|
||||||
|
<div
|
||||||
|
class="invoice-period-order-item-table__product"
|
||||||
|
:style="{ '--order-item-depth': node.meta.tableDepth || 0 }"
|
||||||
|
>
|
||||||
|
<span v-if="node.meta.isAssociatedItem" class="invoice-period-order-item-table__relation" aria-hidden="true">
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
<span v-if="node.meta.isAssociatedItem" class="is-sr-only">
|
||||||
|
{{ treeText("order_items.associated_with", "Tilknyttet") }} {{ node.meta.associatedParentLabel }}
|
||||||
|
</span>
|
||||||
|
<span class="invoice-period-tree-node__label-text" :title="node.label">{{ node.label }}</span>
|
||||||
|
<InvoicingPeriodFlagBadge
|
||||||
|
:key="`${node.id}:title-flags`"
|
||||||
|
:flags="getNodeFlags(node)"
|
||||||
|
@status-changed="handleFlagStatusChanged"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="field in visibleNodeFields(node)"
|
||||||
|
:key="field.key"
|
||||||
|
class="invoice-period-order-item-table__cell"
|
||||||
|
:class="{
|
||||||
|
'is-empty': valueIsEmpty(field.value),
|
||||||
|
'is-editable': field.editable,
|
||||||
|
'is-numeric': field.numeric,
|
||||||
|
}"
|
||||||
|
:data-testid="`invoice-period-tree-field-${node.id}-${field.key}`"
|
||||||
|
>
|
||||||
|
<span class="is-sr-only">{{ field.label }}:</span>
|
||||||
|
<span
|
||||||
|
class="invoice-period-order-item-table__value"
|
||||||
|
:title="field.display"
|
||||||
|
:data-testid="`invoice-period-tree-field-value-${node.id}-${field.key}`"
|
||||||
|
>
|
||||||
|
<EditableTableColumn
|
||||||
|
v-if="field.editable"
|
||||||
|
component-wrapper="span"
|
||||||
|
theme="simple"
|
||||||
|
:object="field.object"
|
||||||
|
:column="field.column"
|
||||||
|
:load-list="refreshAfterInlineEdit"
|
||||||
|
:parse-function="fieldParseFunction(field)"
|
||||||
|
:edit-function="fieldEditFunction(field)"
|
||||||
|
:permission-check-function="() => field.editable === true"
|
||||||
|
:cell-test-id="`invoice-period-tree-field-editor-${node.id}-${field.key}`"
|
||||||
|
/>
|
||||||
|
<span v-else>{{ field.display }}</span>
|
||||||
|
</span>
|
||||||
|
<InvoicingPeriodFlagBadge
|
||||||
|
:key="`${node.id}:${field.key}:flags`"
|
||||||
|
:flags="getFieldFlags(node, field.key)"
|
||||||
|
@status-changed="handleFlagStatusChanged"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="invoice-period-order-item-table__actions" @click.stop @mousedown.stop>
|
||||||
|
<ActionSettingsWheelButton
|
||||||
|
v-if="nodeActionSections(node).length > 0"
|
||||||
|
trigger-button-variant="text"
|
||||||
|
v-bind="nodeActionWheelProps(node)"
|
||||||
|
:menu-sections="nodeActionSections(node)"
|
||||||
|
:data-testid="`invoice-period-tree-action-wheel-${node.id}`"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="invoice-period-tree-node__row">
|
||||||
<div
|
<div
|
||||||
class="invoice-period-tree-node__content"
|
class="invoice-period-tree-node__content"
|
||||||
:class="[
|
:class="[
|
||||||
@@ -3442,12 +3540,15 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
v-for="field in visibleNodeFields(node)"
|
v-for="field in visibleNodeFields(node)"
|
||||||
:key="field.key"
|
:key="field.key"
|
||||||
class="invoice-period-tree-node-field"
|
class="invoice-period-tree-node-field"
|
||||||
:class="{
|
:class="[
|
||||||
'is-empty': valueIsEmpty(field.value),
|
`invoice-period-tree-node-field--${field.key}`,
|
||||||
'is-editable': field.editable,
|
{
|
||||||
'is-numeric': field.numeric,
|
'is-empty': valueIsEmpty(field.value),
|
||||||
'is-composite': field.lines?.length,
|
'is-editable': field.editable,
|
||||||
}"
|
'is-numeric': field.numeric,
|
||||||
|
'is-composite': field.lines?.length,
|
||||||
|
},
|
||||||
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
gridColumn: `${field.slot || 1} / span ${field.span || 1}`,
|
gridColumn: `${field.slot || 1} / span ${field.span || 1}`,
|
||||||
gridRow: field.row ? String(field.row) : undefined,
|
gridRow: field.row ? String(field.row) : undefined,
|
||||||
@@ -3598,6 +3699,7 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BuefyTree>
|
</BuefyTree>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -3608,6 +3710,14 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
padding-top: 0.75rem;
|
padding-top: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-period-object-tree__scroll {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-object-tree__scroll :deep(.b-tree) {
|
||||||
|
min-width: 40rem;
|
||||||
|
}
|
||||||
|
|
||||||
.invoice-period-tree-toolbar {
|
.invoice-period-tree-toolbar {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
@@ -3707,6 +3817,145 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table {
|
||||||
|
min-width: 40rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__header,
|
||||||
|
.invoice-period-order-item-table__row {
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.35rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns:
|
||||||
|
minmax(10rem, 2fr)
|
||||||
|
minmax(6rem, 1fr)
|
||||||
|
minmax(7rem, 1.2fr)
|
||||||
|
minmax(3.75rem, 0.5fr)
|
||||||
|
minmax(5rem, 0.7fr)
|
||||||
|
2.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__header {
|
||||||
|
background: #f3f6f9;
|
||||||
|
border-bottom: 1px solid #d9e1ec;
|
||||||
|
border-top: 1px solid #d9e1ec;
|
||||||
|
color: #687385;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.15;
|
||||||
|
min-height: 1.8rem;
|
||||||
|
padding: 0.25rem 0;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__header > span,
|
||||||
|
.invoice-period-order-item-table__select-all,
|
||||||
|
.invoice-period-order-item-table__cell,
|
||||||
|
.invoice-period-order-item-table__product {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.25rem 0.32rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__select-all {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__select-all input {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__header > span:nth-last-child(-n + 3),
|
||||||
|
.invoice-period-order-item-table__cell.is-numeric,
|
||||||
|
.invoice-period-order-item-table__actions-header {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__row {
|
||||||
|
border-bottom: 1px solid #e7ebf0;
|
||||||
|
min-height: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__product {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding-left: calc(0.32rem + (var(--order-item-depth) * 0.9rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__product .invoice-period-tree-node__label-text {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__relation {
|
||||||
|
color: #7a8699;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__cell {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__cell.is-numeric {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__cell.is-empty .invoice-period-order-item-table__value {
|
||||||
|
color: #8a94a3;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__value {
|
||||||
|
color: #27313d;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__cell.is-numeric .invoice-period-order-item-table__value,
|
||||||
|
.invoice-period-order-item-table__cell.is-numeric :deep(.hover-illustration) {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__cell :deep(.hover-illustration) {
|
||||||
|
color: #27313d;
|
||||||
|
display: block;
|
||||||
|
font-size: 0.76rem;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__actions {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 1.75rem;
|
||||||
|
width: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__actions :deep(.action-settings-wheel-dropdown) {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-order-item-table__actions :deep(.action-settings-wheel-trigger) {
|
||||||
|
height: 1.75rem;
|
||||||
|
width: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.invoice-period-tree-node__row {
|
.invoice-period-tree-node__row {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -3852,7 +4101,7 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (max-width: 38rem) {
|
@container (max-width: 56rem) {
|
||||||
.invoice-period-tree-node__content--with-fields,
|
.invoice-period-tree-node__content--with-fields,
|
||||||
.invoice-period-tree-node__content--order {
|
.invoice-period-tree-node__content--order {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
@@ -3872,7 +4121,16 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.invoice-period-tree-node-field.is-composite {
|
.invoice-period-tree-node-field.is-composite {
|
||||||
grid-column: 1 / -1 !important;
|
grid-column: span 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-tree-node-field--error_message {
|
||||||
|
grid-column: span 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-tree-node-field--total_net_amount,
|
||||||
|
.invoice-period-tree-node-field--price {
|
||||||
|
grid-column: 4 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3883,6 +4141,11 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
.invoice-period-tree-node__fields--collected_order_invoice {
|
.invoice-period-tree-node__fields--collected_order_invoice {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-period-tree-node-field--total_net_amount,
|
||||||
|
.invoice-period-tree-node-field--price {
|
||||||
|
grid-column: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-period-tree-node-field__label > span:first-child {
|
.invoice-period-tree-node-field__label > span:first-child {
|
||||||
@@ -4123,6 +4386,16 @@ const nodeActionWheelProps = (node: TreeNode) => {
|
|||||||
grid-row: auto !important;
|
grid-row: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoice-period-tree-node-field.is-composite,
|
||||||
|
.invoice-period-tree-node-field--error_message {
|
||||||
|
grid-column: 1 / -1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoice-period-tree-node-field--total_net_amount,
|
||||||
|
.invoice-period-tree-node-field--price {
|
||||||
|
grid-column: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
.invoice-period-tree-node__actions {
|
.invoice-period-tree-node__actions {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-5
@@ -80,17 +80,17 @@ const getAttributeTestId = (viewKey: string) => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: repeat(var(--customer-attribute-count), minmax(0, 1fr));
|
grid-template-rows: repeat(var(--customer-attribute-count), minmax(0, 1fr));
|
||||||
gap: 0;
|
gap: 0;
|
||||||
width: max-content;
|
|
||||||
max-width: min(10rem, 18vw);
|
|
||||||
height: 2.45rem;
|
height: 2.45rem;
|
||||||
|
max-width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.customer-attribute-text {
|
.customer-attribute-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -105,7 +105,7 @@ const getAttributeTestId = (viewKey: string) => {
|
|||||||
|
|
||||||
.customer-attribute-text :deep(.icon-text) {
|
.customer-attribute-text :deep(.icon-text) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
@@ -120,7 +120,7 @@ const getAttributeTestId = (viewKey: string) => {
|
|||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.customer-attribute-stack {
|
.customer-attribute-stack {
|
||||||
max-width: 10rem;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
+73
-67
@@ -1178,54 +1178,42 @@ watch(
|
|||||||
@click="(canActivateView(type) ? onClickType(type) : () => {})"
|
@click="(canActivateView(type) ? onClickType(type) : () => {})"
|
||||||
@keydown.enter.prevent="(canActivateView(type) ? onClickType(type) : () => {})"
|
@keydown.enter.prevent="(canActivateView(type) ? onClickType(type) : () => {})"
|
||||||
@keydown.space.prevent="(canActivateView(type) ? onClickType(type) : () => {})">
|
@keydown.space.prevent="(canActivateView(type) ? onClickType(type) : () => {})">
|
||||||
<div class="level is-mobile is-align-items-center">
|
<div class="period-selector-row">
|
||||||
<div class="level-left">
|
<div class="period-selector-label">
|
||||||
<div class="level-item">
|
<ColorIndicator
|
||||||
<ColorIndicator
|
v-bind:label="{
|
||||||
v-bind:label="{
|
text: getSelectorLabelText(type),
|
||||||
text: getSelectorLabelText(type),
|
classes: [],
|
||||||
classes: [],
|
}"
|
||||||
}"
|
v-bind:color_class="getSelectorStatusClass(type)"
|
||||||
v-bind:color_class="getSelectorStatusClass(type)"
|
v-bind:icon_class="getSelectorIconClass(type)"
|
||||||
v-bind:icon_class="getSelectorIconClass(type)"
|
v-bind:visibility="{
|
||||||
v-bind:visibility="{
|
|
||||||
icon: true,
|
icon: true,
|
||||||
dropdown: false,
|
dropdown: false,
|
||||||
}"
|
}"
|
||||||
v-bind:is_loading="isLoading"
|
v-bind:is_loading="isLoading"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="level-right">
|
<div class="period-selector-progress-slot">
|
||||||
<div class="level-item period-selector-progress-slot">
|
<progress class="progress is-small" v-if="isLoading" v-bind:max="1" />
|
||||||
<progress class="progress is-small"
|
<template v-else-if="isViewAvailable(type) && shouldShowSelectorProgress(type)">
|
||||||
v-if="isLoading"
|
<div
|
||||||
v-bind:max="1"
|
class="period-selector-progress"
|
||||||
/>
|
:style="selectorProgressStyle(type)"
|
||||||
<template v-else-if="isViewAvailable(type) && shouldShowSelectorProgress(type)">
|
:data-testid="`invoicing-period-view-selector-progress-${type.name}`"
|
||||||
<div
|
>
|
||||||
class="period-selector-progress"
|
<span class="period-selector-progress__completed"></span>
|
||||||
:style="selectorProgressStyle(type)"
|
<span
|
||||||
:data-testid="`invoicing-period-view-selector-progress-${type.name}`"
|
class="period-selector-progress__draft"
|
||||||
>
|
:data-testid="`invoicing-period-view-selector-progress-draft-${type.name}`"
|
||||||
<span class="period-selector-progress__completed"></span>
|
></span>
|
||||||
<span
|
</div>
|
||||||
class="period-selector-progress__draft"
|
</template>
|
||||||
:data-testid="`invoicing-period-view-selector-progress-draft-${type.name}`"
|
<progress class="progress" v-else-if="!isViewAvailable(type)" v-bind:value="0" v-bind:max="1" />
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<progress class="progress" v-else-if="!isViewAvailable(type)"
|
|
||||||
v-bind:value="0"
|
|
||||||
v-bind:max="1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="level-item">
|
|
||||||
<span class="icon is-small">
|
|
||||||
<i class="fas fa-chevron-right"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="icon is-small period-selector-chevron" aria-hidden="true">
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</WhiteBox>
|
</WhiteBox>
|
||||||
</div>
|
</div>
|
||||||
@@ -1268,53 +1256,71 @@ watch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
.is-selector-view {
|
.is-selector-view {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
min-height: 4rem;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 0.875rem 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-selector-view :deep(.level),
|
.period-selector-row,
|
||||||
.is-selector-view :deep(.level-left),
|
.period-selector-label,
|
||||||
.is-selector-view :deep(.level-right),
|
|
||||||
.is-selector-view :deep(.level-item),
|
|
||||||
.is-selector-view :deep(.color-indicator),
|
.is-selector-view :deep(.color-indicator),
|
||||||
.is-selector-view :deep(.icon-text) {
|
.is-selector-view :deep(.icon-text) {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-selector-view :deep(.level-left) {
|
.period-selector-row {
|
||||||
flex: 1 1 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-selector-view :deep(.level) {
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.35rem;
|
column-gap: 0.75rem;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(84px, 104px);
|
grid-template-columns: minmax(0, 1fr) 72px 1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-selector-view :deep(.level-left .icon-text > span:last-child) {
|
.period-selector-label {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-selector-label :deep(.color-indicator),
|
||||||
|
.period-selector-label :deep(.icon-text) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-selector-label :deep(.icon-text) {
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.5rem minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-selector-label :deep(.icon) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.period-selector-label :deep(.icon-text > span:last-child) {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-selector-view :deep(.level-right) {
|
.period-selector-progress-slot {
|
||||||
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: none;
|
justify-content: stretch;
|
||||||
justify-content: flex-end;
|
min-width: 0;
|
||||||
max-width: 104px;
|
|
||||||
min-width: 84px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-selector-view :deep(.period-selector-progress-slot) {
|
.period-selector-progress-slot > .progress {
|
||||||
flex: 1 1 72px;
|
margin: 0;
|
||||||
max-width: 72px;
|
width: 100%;
|
||||||
min-width: 56px;
|
}
|
||||||
width: auto;
|
|
||||||
|
.period-selector-chevron {
|
||||||
|
justify-self: end;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.period-view-navigation__heading {
|
.period-view-navigation__heading {
|
||||||
|
|||||||
+28
-30
@@ -95,39 +95,14 @@ const formatMonthName = (date, locale = DEFAULT_LOCALE) => capitalizeFirst(new I
|
|||||||
month: "long",
|
month: "long",
|
||||||
}).format(date));
|
}).format(date));
|
||||||
const formatDayMonth = (date, locale = DEFAULT_LOCALE) => `${date.getDate()} ${formatMonthName(date, locale)}`;
|
const formatDayMonth = (date, locale = DEFAULT_LOCALE) => `${date.getDate()} ${formatMonthName(date, locale)}`;
|
||||||
const lastDayOfMonth = (date) => new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
|
|
||||||
const isSameMonth = (left, right) => left && right && left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth();
|
|
||||||
const isWholeMonthRange = (dateFrom, dateTo) => (
|
|
||||||
dateFrom
|
|
||||||
&& dateTo
|
|
||||||
&& isSameMonth(dateFrom, dateTo)
|
|
||||||
&& dateFrom.getDate() === 1
|
|
||||||
&& dateTo.getDate() === lastDayOfMonth(dateFrom)
|
|
||||||
);
|
|
||||||
const getOrderDate = (order) => parseLocalDate(order?.date ?? order?.created_at ?? order?.closed_at ?? order?.updated_at);
|
const getOrderDate = (order) => parseLocalDate(order?.date ?? order?.created_at ?? order?.closed_at ?? order?.updated_at);
|
||||||
|
|
||||||
export const buildRelativeCollectionLabel = ({
|
export const buildRelativeCollectionLabel = ({
|
||||||
collectionId,
|
collectionId,
|
||||||
orders = [],
|
orders = [],
|
||||||
dateFrom = null,
|
|
||||||
dateTo = null,
|
|
||||||
collectionCount = 1,
|
|
||||||
locale = DEFAULT_LOCALE,
|
locale = DEFAULT_LOCALE,
|
||||||
fallbackLabel = null,
|
fallbackLabel = null,
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
const rangeStart = parseLocalDate(dateFrom);
|
|
||||||
const rangeEnd = parseLocalDate(dateTo);
|
|
||||||
|
|
||||||
if (Number(collectionCount) === 1 && isWholeMonthRange(rangeStart, rangeEnd)) {
|
|
||||||
const monthLabel = formatMonthName(rangeStart, locale);
|
|
||||||
return {
|
|
||||||
label: monthLabel,
|
|
||||||
kind: "month",
|
|
||||||
monthLabel,
|
|
||||||
collectionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderDates = (orders || [])
|
const orderDates = (orders || [])
|
||||||
.map(getOrderDate)
|
.map(getOrderDate)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
@@ -247,7 +222,8 @@ export const makeOrderItemNode = (item, options = {}) => {
|
|||||||
children: [],
|
children: [],
|
||||||
selectable: true,
|
selectable: true,
|
||||||
actionable: true,
|
actionable: true,
|
||||||
icon: "fa-list-check",
|
hideToggle: true,
|
||||||
|
hideIcon: true,
|
||||||
meta: {
|
meta: {
|
||||||
item,
|
item,
|
||||||
itemId,
|
itemId,
|
||||||
@@ -331,6 +307,32 @@ export const buildOrderItemTree = (items = [], options = {}) => {
|
|||||||
return roots;
|
return roots;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const flattenOrderItemNodes = (nodes, depth = 0, parent = null) => nodes.flatMap((node) => [
|
||||||
|
{
|
||||||
|
...node,
|
||||||
|
isLeaf: true,
|
||||||
|
children: [],
|
||||||
|
meta: {
|
||||||
|
...node.meta,
|
||||||
|
tableDepth: depth,
|
||||||
|
isAssociatedItem: depth > 0,
|
||||||
|
associatedParentId: parent?.meta?.itemId || null,
|
||||||
|
associatedParentLabel: parent?.label || null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...flattenOrderItemNodes(node.children || [], depth + 1, node),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const buildOrderItemTableRows = (items = [], options = {}) =>
|
||||||
|
flattenOrderItemNodes(buildOrderItemTree(items, options)).map((node, index) => ({
|
||||||
|
...node,
|
||||||
|
meta: {
|
||||||
|
...node.meta,
|
||||||
|
orderId: node.meta.orderId || toPositiveInteger(options.orderId),
|
||||||
|
showTableHeader: index === 0,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
export const classifyAttachment = (attachment) => {
|
export const classifyAttachment = (attachment) => {
|
||||||
const rawName = normalizeString(
|
const rawName = normalizeString(
|
||||||
attachment?.file_name
|
attachment?.file_name
|
||||||
@@ -514,7 +516,6 @@ export const buildCollectionRootNodes = (customer, transactions = [], excludedOr
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const collectionCount = groups.size;
|
|
||||||
const nodes = Array.from(groups.entries())
|
const nodes = Array.from(groups.entries())
|
||||||
.sort(([left], [right]) => left - right)
|
.sort(([left], [right]) => left - right)
|
||||||
.map(([collectionId, orders]) => {
|
.map(([collectionId, orders]) => {
|
||||||
@@ -522,9 +523,6 @@ export const buildCollectionRootNodes = (customer, transactions = [], excludedOr
|
|||||||
const relativeLabel = buildRelativeCollectionLabel({
|
const relativeLabel = buildRelativeCollectionLabel({
|
||||||
collectionId,
|
collectionId,
|
||||||
orders,
|
orders,
|
||||||
dateFrom: labels.dateFrom,
|
|
||||||
dateTo: labels.dateTo,
|
|
||||||
collectionCount,
|
|
||||||
locale: labels.locale,
|
locale: labels.locale,
|
||||||
fallbackLabel,
|
fallbackLabel,
|
||||||
});
|
});
|
||||||
|
|||||||
+16
-8
@@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
TREE_CATEGORY_TYPES,
|
TREE_CATEGORY_TYPES,
|
||||||
TREE_NODE_TYPES,
|
TREE_NODE_TYPES,
|
||||||
buildOrderItemTree,
|
buildRelativeCollectionLabel,
|
||||||
|
buildOrderItemTableRows,
|
||||||
classifyAttachment,
|
classifyAttachment,
|
||||||
getTreeAmount,
|
getTreeAmount,
|
||||||
makeAttachmentNode,
|
makeAttachmentNode,
|
||||||
@@ -78,6 +79,7 @@ export type InvoicingPeriodTreeSnapshot =
|
|||||||
|
|
||||||
export interface SnapshotTreeLabels {
|
export interface SnapshotTreeLabels {
|
||||||
collection: (id: number) => string;
|
collection: (id: number) => string;
|
||||||
|
locale?: string;
|
||||||
ordersWithoutCollection: string;
|
ordersWithoutCollection: string;
|
||||||
orders: string;
|
orders: string;
|
||||||
items: string;
|
items: string;
|
||||||
@@ -258,9 +260,10 @@ const buildBookingNode = (booking: Record<string, any>, labels: SnapshotTreeLabe
|
|||||||
const buildCompleteOrderNode = (order: InvoicingPeriodSnapshotOrder, labels: SnapshotTreeLabels) => {
|
const buildCompleteOrderNode = (order: InvoicingPeriodSnapshotOrder, labels: SnapshotTreeLabels) => {
|
||||||
const node = makeOrderNode(order, { label: labels.order(order.id) });
|
const node = makeOrderNode(order, { label: labels.order(order.id) });
|
||||||
node.meta.inSelectedPeriod = order.in_selected_period;
|
node.meta.inSelectedPeriod = order.in_selected_period;
|
||||||
const itemNodes = buildOrderItemTree(order.items, {
|
const itemNodes = buildOrderItemTableRows(order.items, {
|
||||||
fallbackLabel: (item: any, index: number) => labels.orderItemFallback(positiveInteger(item?.id) ?? index + 1),
|
fallbackLabel: (item: any, index: number) => labels.orderItemFallback(positiveInteger(item?.id) ?? index + 1),
|
||||||
invoiceState: order.invoice_state,
|
invoiceState: order.invoice_state,
|
||||||
|
orderId: order.id,
|
||||||
});
|
});
|
||||||
const attachmentGroups = new Map<string, any[]>([["certificate", []], ["image", []], ["other", []]]);
|
const attachmentGroups = new Map<string, any[]>([["certificate", []], ["image", []], ["other", []]]);
|
||||||
order.attachments.forEach((attachment: any, index: number) => {
|
order.attachments.forEach((attachment: any, index: number) => {
|
||||||
@@ -281,9 +284,6 @@ const buildCompleteOrderNode = (order: InvoicingPeriodSnapshotOrder, labels: Sna
|
|||||||
return xlvaskNode;
|
return xlvaskNode;
|
||||||
});
|
});
|
||||||
const categories = [
|
const categories = [
|
||||||
itemNodes.length > 0
|
|
||||||
? category(makeNodeId(TREE_NODE_TYPES.CATEGORY, order.id, TREE_CATEGORY_TYPES.ORDER_ITEMS), labels.items, TREE_CATEGORY_TYPES.ORDER_ITEMS, node.type, node.id, itemNodes)
|
|
||||||
: null,
|
|
||||||
...(["certificate", "image", "other"] as const).map((kind) => {
|
...(["certificate", "image", "other"] as const).map((kind) => {
|
||||||
const children = attachmentGroups.get(kind) ?? [];
|
const children = attachmentGroups.get(kind) ?? [];
|
||||||
const categoryType = {
|
const categoryType = {
|
||||||
@@ -302,8 +302,8 @@ const buildCompleteOrderNode = (order: InvoicingPeriodSnapshotOrder, labels: Sna
|
|||||||
? category(makeNodeId(TREE_NODE_TYPES.CATEGORY, order.id, TREE_CATEGORY_TYPES.ORDER_XLVASK), labels.xlvask, TREE_CATEGORY_TYPES.ORDER_XLVASK, node.type, node.id, xlvaskNodes)
|
? category(makeNodeId(TREE_NODE_TYPES.CATEGORY, order.id, TREE_CATEGORY_TYPES.ORDER_XLVASK), labels.xlvask, TREE_CATEGORY_TYPES.ORDER_XLVASK, node.type, node.id, xlvaskNodes)
|
||||||
: null,
|
: null,
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
node.children = categories;
|
node.children = [...itemNodes, ...categories];
|
||||||
node.isLeaf = categories.length === 0;
|
node.isLeaf = node.children.length === 0;
|
||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -325,8 +325,16 @@ const genericObjectNode = (type: string, object: Record<string, any>, label: str
|
|||||||
export const buildCompleteSnapshotRootNodes = (snapshot: CompleteInvoicingPeriodTreeSnapshot, labels: SnapshotTreeLabels) => {
|
export const buildCompleteSnapshotRootNodes = (snapshot: CompleteInvoicingPeriodTreeSnapshot, labels: SnapshotTreeLabels) => {
|
||||||
const customer = snapshot.customer;
|
const customer = snapshot.customer;
|
||||||
const collectionNodes = [...snapshot.collections].sort((left, right) => left.id - right.id).map((collection) => {
|
const collectionNodes = [...snapshot.collections].sort((left, right) => left.id - right.id).map((collection) => {
|
||||||
|
const fallbackLabel = labels.collection(collection.id);
|
||||||
|
const relativeLabel = buildRelativeCollectionLabel({
|
||||||
|
collectionId: collection.id,
|
||||||
|
orders: collection.orders.filter((order) => order.in_selected_period),
|
||||||
|
locale: labels.locale,
|
||||||
|
fallbackLabel,
|
||||||
|
});
|
||||||
const node = makeCollectionNode(collection.id, collection.orders, customer, {
|
const node = makeCollectionNode(collection.id, collection.orders, customer, {
|
||||||
label: labels.collection(collection.id),
|
label: relativeLabel?.label || fallbackLabel,
|
||||||
|
relativeLabel,
|
||||||
collectionSummary: collection,
|
collectionSummary: collection,
|
||||||
invoiceState: asString(collection.invoice_state ?? collection.state),
|
invoiceState: asString(collection.invoice_state ?? collection.state),
|
||||||
completeOrderCount: collection.complete_order_count,
|
completeOrderCount: collection.complete_order_count,
|
||||||
|
|||||||
+28
-10
@@ -1757,7 +1757,8 @@ const getTransactionQueryParameters = () => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"identity identity settings"
|
"identity identity settings"
|
||||||
"attributes count amount"
|
"count amount amount"
|
||||||
|
"attributes attributes attributes"
|
||||||
"activity state state";
|
"activity state state";
|
||||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
@@ -1765,14 +1766,14 @@ const getTransactionQueryParameters = () => {
|
|||||||
padding: 0.25rem;
|
padding: 0.25rem;
|
||||||
}
|
}
|
||||||
.period-customer-card__row > .column { min-width: 0; padding: 0.35rem; }
|
.period-customer-card__row > .column { min-width: 0; padding: 0.35rem; }
|
||||||
.period-customer-card__identity { min-width: 0; }
|
.period-customer-card__identity { min-width: 0; overflow: hidden; }
|
||||||
.period-customer-card__identity { grid-area: identity; }
|
.period-customer-card__identity { grid-area: identity; }
|
||||||
.period-customer-card__attributes { grid-area: attributes; }
|
.period-customer-card__attributes { grid-area: attributes; justify-self: stretch; width: 100%; }
|
||||||
.period-customer-card__activity { grid-area: activity; }
|
.period-customer-card__activity { grid-area: activity; }
|
||||||
.period-customer-card__count { grid-area: count; white-space: nowrap; }
|
.period-customer-card__count { grid-area: count; justify-self: end; white-space: nowrap; }
|
||||||
.period-customer-card__amount { grid-area: amount; white-space: nowrap; }
|
.period-customer-card__amount { grid-area: amount; justify-self: end; white-space: nowrap; }
|
||||||
.period-customer-card__state { grid-area: state; justify-self: end; }
|
.period-customer-card__state { grid-area: state; justify-self: end; }
|
||||||
.period-customer-card__settings { grid-area: settings; }
|
.period-customer-card__settings { align-self: start; grid-area: settings; justify-self: end; }
|
||||||
.period-customer-card__selector {
|
.period-customer-card__selector {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -1788,6 +1789,13 @@ const getTransactionQueryParameters = () => {
|
|||||||
.period-customer-card__selector:focus-visible { box-shadow: 0 0 0 3px rgba(50, 115, 220, 0.28); outline: none; }
|
.period-customer-card__selector:focus-visible { box-shadow: 0 0 0 3px rgba(50, 115, 220, 0.28); outline: none; }
|
||||||
.period-customer-card__selector :deep(.color-indicator),
|
.period-customer-card__selector :deep(.color-indicator),
|
||||||
.period-customer-card__selector :deep(.icon-text) { min-width: 0; }
|
.period-customer-card__selector :deep(.icon-text) { min-width: 0; }
|
||||||
|
.period-customer-card__selector :deep(.icon-text) {
|
||||||
|
align-items: center;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
grid-template-columns: 1.25rem minmax(0, 1fr);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
.period-customer-card__selector :deep(.icon-text > span:last-child) {
|
.period-customer-card__selector :deep(.icon-text > span:last-child) {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1798,13 +1806,22 @@ const getTransactionQueryParameters = () => {
|
|||||||
.period-customer-review {
|
.period-customer-review {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: #485466;
|
color: #485466;
|
||||||
display: flex;
|
display: grid;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
grid-template-columns: 1.25rem auto minmax(0, 1fr);
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.period-customer-review__next { color: #6b7280; margin-left: 0.25rem; }
|
.period-customer-review__next {
|
||||||
|
color: #6b7280;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.review-state--red { color: #b42318; }
|
.review-state--red { color: #b42318; }
|
||||||
.review-state--yellow { color: #9a6700; }
|
.review-state--yellow { color: #9a6700; }
|
||||||
.review-state--blue { color: #175cd3; }
|
.review-state--blue { color: #175cd3; }
|
||||||
@@ -1873,8 +1890,9 @@ const getTransactionQueryParameters = () => {
|
|||||||
.period-customer-card__row {
|
.period-customer-card__row {
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
"identity identity settings"
|
"identity identity settings"
|
||||||
"attributes count amount"
|
"count amount amount"
|
||||||
"attributes state state";
|
"attributes attributes attributes"
|
||||||
|
"state state state";
|
||||||
}
|
}
|
||||||
.period-customer-card__attributes { align-self: stretch; grid-area: attributes; }
|
.period-customer-card__attributes { align-self: stretch; grid-area: attributes; }
|
||||||
.period-customer-card__activity { display: none; }
|
.period-customer-card__activity { display: none; }
|
||||||
|
|||||||
@@ -167,8 +167,9 @@ function createObjectTreePeriodPayload({ dateFrom = "2026-07-14" } = {}) {
|
|||||||
customer_number: 4101,
|
customer_number: 4101,
|
||||||
customer_name: "Object Tree Logistics",
|
customer_name: "Object Tree Logistics",
|
||||||
cashier_id: 7,
|
cashier_id: 7,
|
||||||
cashier_name: "Copenhagen",
|
cashier_name: "Jeppe",
|
||||||
department_id: 1,
|
department_id: 1,
|
||||||
|
department_name: "Copenhagen",
|
||||||
invoice_collection_id: 3001,
|
invoice_collection_id: 3001,
|
||||||
invoice_collection: {
|
invoice_collection: {
|
||||||
id: 3001,
|
id: 3001,
|
||||||
@@ -1465,6 +1466,107 @@ test.describe("Invoicing period tab", () => {
|
|||||||
expect(periodRequests.length).toBeGreaterThan(0);
|
expect(periodRequests.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("@pr period view keeps every right-rail selector column aligned", async ({ page }) => {
|
||||||
|
await page.setViewportSize({ width: 1920, height: 1080 });
|
||||||
|
await openPeriodView(page);
|
||||||
|
|
||||||
|
const selectorNames = [
|
||||||
|
"all",
|
||||||
|
"possible_duplicates",
|
||||||
|
"invoice_per_order",
|
||||||
|
"fixed_pricing",
|
||||||
|
"special_arrangements",
|
||||||
|
"vehicle_subscriptions",
|
||||||
|
"tank_cleaning",
|
||||||
|
"self_wash",
|
||||||
|
];
|
||||||
|
|
||||||
|
const readRailGeometry = async () => {
|
||||||
|
const selectors = selectorNames.map((name) => page.getByTestId(`invoicing-period-view-selector-${name}`));
|
||||||
|
await Promise.all(selectors.map((selector) => expect(selector).toBeVisible()));
|
||||||
|
|
||||||
|
return page.getByTestId("invoicing-period-view-selectors").evaluate((navigation, names) => {
|
||||||
|
const readRect = (element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: rect.x,
|
||||||
|
y: rect.y,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height,
|
||||||
|
centerY: rect.y + rect.height / 2,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const selectorsByName = names.map((name) => {
|
||||||
|
const selector = navigation.querySelector(`[data-testid="invoicing-period-view-selector-${name}"]`);
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
selector: readRect(selector),
|
||||||
|
row: readRect(selector.querySelector(".period-selector-row")),
|
||||||
|
icon: readRect(selector.querySelector(".period-selector-label .icon")),
|
||||||
|
label: readRect(selector.querySelector(".period-selector-label .icon-text > span:last-child")),
|
||||||
|
progress: readRect(selector.querySelector(".period-selector-progress-slot")),
|
||||||
|
chevron: readRect(selector.querySelector(".period-selector-chevron")),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const groups = [...navigation.querySelectorAll(".period-view-navigation__group")].map((group) => {
|
||||||
|
const heading = readRect(group.querySelector(".period-view-navigation__heading"));
|
||||||
|
const cards = [...group.querySelectorAll(".is-selector-view")].map(readRect);
|
||||||
|
return { heading, cards };
|
||||||
|
});
|
||||||
|
return { selectors: selectorsByName, groups };
|
||||||
|
}, selectorNames);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const viewportWidth of [1920, 1280]) {
|
||||||
|
await page.setViewportSize({ width: viewportWidth, height: 1080 });
|
||||||
|
const geometry = await readRailGeometry();
|
||||||
|
const first = geometry.selectors[0];
|
||||||
|
|
||||||
|
for (const item of geometry.selectors) {
|
||||||
|
expect(
|
||||||
|
Math.abs(item.selector.height - first.selector.height),
|
||||||
|
`${viewportWidth}px ${item.name} height`
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
expect(Math.abs(item.row.x - first.row.x), `${viewportWidth}px ${item.name} row left`).toBeLessThanOrEqual(1);
|
||||||
|
expect(Math.abs(item.icon.x - first.icon.x), `${viewportWidth}px ${item.name} icon left`).toBeLessThanOrEqual(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
Math.abs(item.label.x - first.label.x),
|
||||||
|
`${viewportWidth}px ${item.name} label left`
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
expect(
|
||||||
|
Math.abs(item.progress.x - first.progress.x),
|
||||||
|
`${viewportWidth}px ${item.name} progress left`
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
expect(
|
||||||
|
Math.abs(item.progress.width - first.progress.width),
|
||||||
|
`${viewportWidth}px ${item.name} progress width`
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
expect(
|
||||||
|
Math.abs(item.chevron.x - first.chevron.x),
|
||||||
|
`${viewportWidth}px ${item.name} chevron left`
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
expect(
|
||||||
|
Math.abs(item.row.centerY - item.selector.centerY),
|
||||||
|
`${viewportWidth}px ${item.name} vertical center`
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cardGaps = [];
|
||||||
|
for (const group of geometry.groups) {
|
||||||
|
expect(
|
||||||
|
Math.abs(group.heading.x - group.cards[0].x),
|
||||||
|
`${viewportWidth}px heading left edge`
|
||||||
|
).toBeLessThanOrEqual(1);
|
||||||
|
for (let index = 1; index < group.cards.length; index += 1) {
|
||||||
|
cardGaps.push(group.cards[index].y - (group.cards[index - 1].y + group.cards[index - 1].height));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(Math.max(...cardGaps) - Math.min(...cardGaps), `${viewportWidth}px card gaps`).toBeLessThanOrEqual(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("@smoke @pr period review workspace restores filters and supports keyboard master-detail navigation", async ({
|
test("@smoke @pr period review workspace restores filters and supports keyboard master-detail navigation", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -1601,6 +1703,14 @@ test.describe("Invoicing period tab", () => {
|
|||||||
beforeGoto: (currentPage) => routeObjectTreeV2Endpoints(currentPage, state),
|
beforeGoto: (currentPage) => routeObjectTreeV2Endpoints(currentPage, state),
|
||||||
});
|
});
|
||||||
await page.getByTestId("invoicing-period-view-selector-all").click();
|
await page.getByTestId("invoicing-period-view-selector-all").click();
|
||||||
|
const snapshotCollection = page.getByTestId("invoice-period-tree-node-collected_order_invoice:3001");
|
||||||
|
await expect(snapshotCollection.locator(".invoice-period-tree-node__label-text")).toContainText(/1 Juli|1 July/i);
|
||||||
|
await expect(snapshotCollection.locator(".invoice-period-tree-node__label-text")).not.toContainText(
|
||||||
|
/Fakturasamling|Collection\s+#/i
|
||||||
|
);
|
||||||
|
await expect(snapshotCollection.locator(".invoice-period-tree-node__label-text")).not.toContainText(
|
||||||
|
/29 Juni|29 June/i
|
||||||
|
);
|
||||||
await expect(page.getByTestId("invoice-period-tree-node-collected_order_invoice:3001")).toContainText(
|
await expect(page.getByTestId("invoice-period-tree-node-collected_order_invoice:3001")).toContainText(
|
||||||
/Valgt periode|Selected period/i
|
/Valgt periode|Selected period/i
|
||||||
);
|
);
|
||||||
@@ -1610,6 +1720,9 @@ test.describe("Invoicing period tab", () => {
|
|||||||
|
|
||||||
await page.getByTestId("invoice-period-tree-toggle-all").click();
|
await page.getByTestId("invoice-period-tree-toggle-all").click();
|
||||||
await expect(page.getByTestId("invoice-period-tree-node-order:9002")).toBeVisible();
|
await expect(page.getByTestId("invoice-period-tree-node-order:9002")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("invoice-period-tree-node-order_item:7701")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("invoice-period-order-item-table-header-9001")).toHaveCount(1);
|
||||||
|
await expect(page.locator('[data-node-key="category:9001:order_items"]')).toHaveCount(0);
|
||||||
expect(orderItemRequests).toHaveLength(0);
|
expect(orderItemRequests).toHaveLength(0);
|
||||||
|
|
||||||
const collectionNode = page.locator('[data-node-key="collected_order_invoice:3001"]');
|
const collectionNode = page.locator('[data-node-key="collected_order_invoice:3001"]');
|
||||||
@@ -1930,6 +2043,21 @@ test.describe("Invoicing period tab", () => {
|
|||||||
await expect(customerRow.getByText(/Alle bogført|All booked/i)).toHaveCount(0);
|
await expect(customerRow.getByText(/Alle bogført|All booked/i)).toHaveCount(0);
|
||||||
await expect(customerRow.getByText(/Spot Free/)).toHaveCount(0);
|
await expect(customerRow.getByText(/Spot Free/)).toHaveCount(0);
|
||||||
const manualFlag = page.getByTestId("invoice-period-flag-501");
|
const manualFlag = page.getByTestId("invoice-period-flag-501");
|
||||||
|
const manualFlagBox = await getBoundingBox(manualFlag, "customer flag row");
|
||||||
|
const manualFlagMessage = manualFlag.locator(".invoice-period-flag-row__message");
|
||||||
|
const manualFlagMessageBox = await getBoundingBox(manualFlagMessage, "customer flag message");
|
||||||
|
expect(manualFlagMessageBox.width).toBeGreaterThan(manualFlagBox.width * 0.7);
|
||||||
|
expect(manualFlagMessageBox.x).toBeGreaterThan(manualFlagBox.x);
|
||||||
|
await expect(manualFlagMessage).toHaveCSS("text-align", "left");
|
||||||
|
const manualFlagActions = manualFlag.locator(".invoice-period-flag-row__actions");
|
||||||
|
await expect(manualFlagActions).toHaveCSS("opacity", "0");
|
||||||
|
await manualFlagActions.getByTitle(/Resolved|Løst/i).focus();
|
||||||
|
await expect(manualFlagActions).toHaveCSS("opacity", "1");
|
||||||
|
const revealedManualFlagMessageBox = await getBoundingBox(manualFlagMessage, "focused customer flag message");
|
||||||
|
const revealedManualFlagActionsBox = await getBoundingBox(manualFlagActions, "focused customer flag actions");
|
||||||
|
expect(revealedManualFlagActionsBox.y).toBeGreaterThanOrEqual(
|
||||||
|
revealedManualFlagMessageBox.y + revealedManualFlagMessageBox.height - 1
|
||||||
|
);
|
||||||
await manualFlag.locator(".invoice-period-flag-row__icon").hover();
|
await manualFlag.locator(".invoice-period-flag-row__icon").hover();
|
||||||
const manualFlagTooltip = page.getByTestId("invoice-period-flag-created-tooltip-501");
|
const manualFlagTooltip = page.getByTestId("invoice-period-flag-created-tooltip-501");
|
||||||
await expect(manualFlagTooltip).toContainText("Oprettet");
|
await expect(manualFlagTooltip).toContainText("Oprettet");
|
||||||
@@ -2143,6 +2271,38 @@ test.describe("Invoicing period tab", () => {
|
|||||||
await expandTreeNode(page, "category:3001:collection_orders");
|
await expandTreeNode(page, "category:3001:collection_orders");
|
||||||
await expandTreeNode(page, "order:9001");
|
await expandTreeNode(page, "order:9001");
|
||||||
|
|
||||||
|
const collectionFieldRows = await Promise.all(
|
||||||
|
[
|
||||||
|
["state", "po_number", "notes", "booked_invoice_id"],
|
||||||
|
["external_id", "closed_at", "processor", "total_net_amount"],
|
||||||
|
["error_message", "created_at", "updated_at"],
|
||||||
|
].map((fields) =>
|
||||||
|
Promise.all(
|
||||||
|
fields.map((field) =>
|
||||||
|
getBoundingBox(
|
||||||
|
page.getByTestId(`invoice-period-tree-field-collected_order_invoice:3001-${field}`),
|
||||||
|
`collection ${field}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
collectionFieldRows.forEach((row, rowIndex) => {
|
||||||
|
const rowTops = row.map((box) => Math.round(box.y));
|
||||||
|
expect(
|
||||||
|
Math.max(...rowTops) - Math.min(...rowTops),
|
||||||
|
`collection metadata row ${rowIndex + 1}`
|
||||||
|
).toBeLessThanOrEqual(2);
|
||||||
|
});
|
||||||
|
const primaryCollectionFields = [...collectionFieldRows[0], ...collectionFieldRows[1]];
|
||||||
|
const primaryCollectionWidths = primaryCollectionFields.map((box) => Math.round(box.width));
|
||||||
|
expect(Math.max(...primaryCollectionWidths) - Math.min(...primaryCollectionWidths)).toBeLessThanOrEqual(2);
|
||||||
|
expect(Math.min(...primaryCollectionWidths)).toBeGreaterThanOrEqual(60);
|
||||||
|
const usesCompactMetadataGrid = collectionFieldRows[1][0].y > collectionFieldRows[0][0].y + 2;
|
||||||
|
const lastPrimaryRowY = usesCompactMetadataGrid ? collectionFieldRows[1][0].y : collectionFieldRows[0][0].y;
|
||||||
|
expect(collectionFieldRows[2][0].y).toBeGreaterThan(lastPrimaryRowY);
|
||||||
|
expect(collectionFieldRows[2][0].width).toBeGreaterThan(collectionFieldRows[2][1].width * 1.8);
|
||||||
|
|
||||||
const wheelLocators = [
|
const wheelLocators = [
|
||||||
page.getByTestId("invoice-period-tree-action-wheel-collected_order_invoice:3001"),
|
page.getByTestId("invoice-period-tree-action-wheel-collected_order_invoice:3001"),
|
||||||
page.getByTestId("invoice-period-tree-action-wheel-order:9001"),
|
page.getByTestId("invoice-period-tree-action-wheel-order:9001"),
|
||||||
@@ -2156,6 +2316,10 @@ test.describe("Invoicing period tab", () => {
|
|||||||
expect(Math.max(...wheelRightEdges) - Math.min(...wheelRightEdges)).toBeLessThanOrEqual(6);
|
expect(Math.max(...wheelRightEdges) - Math.min(...wheelRightEdges)).toBeLessThanOrEqual(6);
|
||||||
|
|
||||||
await expect(page.getByTestId("invoice-period-tree-field-order:9001-notes")).toBeVisible();
|
await expect(page.getByTestId("invoice-period-tree-field-order:9001-notes")).toBeVisible();
|
||||||
|
await expect(page.getByTestId("invoice-period-tree-field-order:9001-department")).toContainText(
|
||||||
|
/Afdeling|Department/i
|
||||||
|
);
|
||||||
|
await expect(page.getByTestId("invoice-period-tree-field-value-order:9001-department")).toHaveText("Copenhagen");
|
||||||
await expect(page.getByTestId("invoice-period-tree-field-order:9001-registrations")).toBeVisible();
|
await expect(page.getByTestId("invoice-period-tree-field-order:9001-registrations")).toBeVisible();
|
||||||
await expect(page.getByTestId("invoice-period-tree-field-order:9001-reg_2")).toBeVisible();
|
await expect(page.getByTestId("invoice-period-tree-field-order:9001-reg_2")).toBeVisible();
|
||||||
await expect(page.getByTestId("invoice-period-tree-field-order:9001-reg_3")).toHaveCount(0);
|
await expect(page.getByTestId("invoice-period-tree-field-order:9001-reg_3")).toHaveCount(0);
|
||||||
@@ -2164,13 +2328,33 @@ test.describe("Invoicing period tab", () => {
|
|||||||
await expect(page.getByTestId("invoice-period-tree-node-order:9001")).toContainText("Vask #9001");
|
await expect(page.getByTestId("invoice-period-tree-node-order:9001")).toContainText("Vask #9001");
|
||||||
await expect(page.locator("[data-testid$='-product_id']")).toHaveCount(0);
|
await expect(page.locator("[data-testid$='-product_id']")).toHaveCount(0);
|
||||||
|
|
||||||
const fieldBoxes = await Promise.all(
|
const firstOrderFieldRow = await Promise.all(
|
||||||
["reference", "notes", "po", "registrations", "include_in_invoice", "total_net_amount"].map((field) =>
|
["reference", "po", "notes", "department"].map((field) =>
|
||||||
getBoundingBox(page.getByTestId(`invoice-period-tree-field-order:9001-${field}`), field)
|
getBoundingBox(page.getByTestId(`invoice-period-tree-field-order:9001-${field}`), field)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const fieldTops = fieldBoxes.map((box) => Math.round(box.y));
|
const firstOrderFieldTops = firstOrderFieldRow.map((box) => Math.round(box.y));
|
||||||
expect(Math.max(...fieldTops) - Math.min(...fieldTops)).toBeLessThanOrEqual(2);
|
expect(Math.max(...firstOrderFieldTops) - Math.min(...firstOrderFieldTops)).toBeLessThanOrEqual(2);
|
||||||
|
const firstOrderFieldWidths = firstOrderFieldRow.map((box) => Math.round(box.width));
|
||||||
|
expect(Math.max(...firstOrderFieldWidths) - Math.min(...firstOrderFieldWidths)).toBeLessThanOrEqual(2);
|
||||||
|
|
||||||
|
const registrationBox = await getBoundingBox(
|
||||||
|
page.getByTestId("invoice-period-tree-field-order:9001-registrations"),
|
||||||
|
"registrations"
|
||||||
|
);
|
||||||
|
const includeInInvoiceBox = await getBoundingBox(
|
||||||
|
page.getByTestId("invoice-period-tree-field-order:9001-include_in_invoice"),
|
||||||
|
"include in invoice"
|
||||||
|
);
|
||||||
|
const orderTotalBox = await getBoundingBox(
|
||||||
|
page.getByTestId("invoice-period-tree-field-order:9001-total_net_amount"),
|
||||||
|
"order total"
|
||||||
|
);
|
||||||
|
expect(Math.abs(registrationBox.y - includeInInvoiceBox.y)).toBeLessThanOrEqual(2);
|
||||||
|
expect(Math.abs(orderTotalBox.y - includeInInvoiceBox.y)).toBeLessThanOrEqual(2);
|
||||||
|
expect(registrationBox.y).toBeGreaterThan(firstOrderFieldRow[0].y);
|
||||||
|
expect(registrationBox.width).toBeGreaterThan(includeInInvoiceBox.width * 1.8);
|
||||||
|
expect(Math.abs(orderTotalBox.width - includeInInvoiceBox.width)).toBeLessThanOrEqual(2);
|
||||||
|
|
||||||
const quantityBox = await getBoundingBox(
|
const quantityBox = await getBoundingBox(
|
||||||
page.getByTestId("invoice-period-tree-field-order_item:7701-quantity"),
|
page.getByTestId("invoice-period-tree-field-order_item:7701-quantity"),
|
||||||
@@ -2178,11 +2362,29 @@ test.describe("Invoicing period tab", () => {
|
|||||||
);
|
);
|
||||||
const priceBox = await getBoundingBox(page.getByTestId("invoice-period-tree-field-order_item:7701-price"), "price");
|
const priceBox = await getBoundingBox(page.getByTestId("invoice-period-tree-field-order_item:7701-price"), "price");
|
||||||
const firstOrderItem = page.getByTestId("invoice-period-tree-node-order_item:7701");
|
const firstOrderItem = page.getByTestId("invoice-period-tree-node-order_item:7701");
|
||||||
|
const associatedOrderItem = page.getByTestId("invoice-period-tree-node-order_item:7702");
|
||||||
|
const itemTableHeader = page.getByTestId("invoice-period-order-item-table-header-9001");
|
||||||
await expect(firstOrderItem.locator(".invoice-period-tree-node__subtitle")).toHaveCount(0);
|
await expect(firstOrderItem.locator(".invoice-period-tree-node__subtitle")).toHaveCount(0);
|
||||||
await expect(page.getByTestId("invoice-period-tree-field-order_item:7701-quantity")).toContainText(
|
await expect(itemTableHeader).toHaveCount(1);
|
||||||
/Antal|Quantity/i
|
await expect(itemTableHeader).toContainText(/Produkt|Product/i);
|
||||||
);
|
await expect(itemTableHeader).toContainText(/Antal|Quantity/i);
|
||||||
await expect(page.getByTestId("invoice-period-tree-field-order_item:7701-price")).toContainText(/Pris|Price/i);
|
await expect(itemTableHeader).toContainText(/Pris|Price/i);
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId("invoice-period-tree-field-order_item:7701-quantity")
|
||||||
|
.locator(".invoice-period-order-item-table__value")
|
||||||
|
).not.toContainText(/Antal|Quantity/i);
|
||||||
|
await expect(
|
||||||
|
page
|
||||||
|
.getByTestId("invoice-period-tree-field-order_item:7701-price")
|
||||||
|
.locator(".invoice-period-order-item-table__value")
|
||||||
|
).not.toContainText(/Pris|Price/i);
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("invoice-period-tree-field-order_item:7701-quantity").locator(".is-sr-only")
|
||||||
|
).toContainText(/Antal|Quantity/i);
|
||||||
|
await expect(
|
||||||
|
page.getByTestId("invoice-period-tree-field-order_item:7702-reference").locator(".is-sr-only")
|
||||||
|
).toContainText(/Reference/i);
|
||||||
expect(quantityBox.x + quantityBox.width).toBeLessThanOrEqual(priceBox.x + 2);
|
expect(quantityBox.x + quantityBox.width).toBeLessThanOrEqual(priceBox.x + 2);
|
||||||
const alignedAmountBoxes = await Promise.all([
|
const alignedAmountBoxes = await Promise.all([
|
||||||
getBoundingBox(
|
getBoundingBox(
|
||||||
@@ -2194,12 +2396,53 @@ test.describe("Invoicing period tab", () => {
|
|||||||
]);
|
]);
|
||||||
const amountRightEdges = alignedAmountBoxes.map((box) => Math.round(box.x + box.width));
|
const amountRightEdges = alignedAmountBoxes.map((box) => Math.round(box.x + box.width));
|
||||||
expect(Math.max(...amountRightEdges) - Math.min(...amountRightEdges)).toBeLessThanOrEqual(8);
|
expect(Math.max(...amountRightEdges) - Math.min(...amountRightEdges)).toBeLessThanOrEqual(8);
|
||||||
await expandTreeNode(page, "order_item:7701");
|
await expect(associatedOrderItem).toBeVisible();
|
||||||
await expect(page.locator('[data-node-key="order_item:7701"] [data-node-key="order_item:7702"]')).toBeVisible();
|
await expect(
|
||||||
|
page.locator('[data-node-key="order:9001"] > .b-tree-children > [data-node-key="order_item:7702"]')
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.locator('[data-node-key="category:9001:order_items"]')).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-node-key="order_item:7701"] > .b-tree-node-content > .b-tree-node-icon')
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-node-key="order_item:7702"] > .b-tree-node-content > .b-tree-node-icon')
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-node-key="order_item:7701"] > .b-tree-node-content > .b-tree-node-toggle')
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.locator('[data-node-key="order_item:7702"] > .b-tree-node-content > .b-tree-node-toggle')
|
||||||
|
).toHaveCount(0);
|
||||||
|
await expect(associatedOrderItem.locator(".invoice-period-order-item-table__relation")).toHaveText("+");
|
||||||
|
const associatedItemAriaSnapshot = await page.locator('[data-node-key="order_item:7702"]').ariaSnapshot();
|
||||||
|
expect(associatedItemAriaSnapshot).toMatch(/Tilknyttet|Associated/i);
|
||||||
|
expect(associatedItemAriaSnapshot).toContain("Forvogn med ekstra langt produktnavn til visuel afkortning");
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("invoice-period-tree-node-order_item:7701").locator(".invoice-period-tree-node__label-text")
|
page.getByTestId("invoice-period-tree-node-order_item:7701").locator(".invoice-period-tree-node__label-text")
|
||||||
).toHaveCSS("white-space", "normal");
|
).toHaveCSS("white-space", "normal");
|
||||||
|
|
||||||
|
const rootItemColumns = await Promise.all(
|
||||||
|
["reference", "notes", "quantity", "price"].map((field) =>
|
||||||
|
getBoundingBox(page.getByTestId(`invoice-period-tree-field-order_item:7701-${field}`), `root item ${field}`)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const associatedItemColumns = await Promise.all(
|
||||||
|
["reference", "notes", "quantity", "price"].map((field) =>
|
||||||
|
getBoundingBox(
|
||||||
|
page.getByTestId(`invoice-period-tree-field-order_item:7702-${field}`),
|
||||||
|
`associated item ${field}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
rootItemColumns.forEach((box, index) => {
|
||||||
|
expect(Math.abs(box.x - associatedItemColumns[index].x)).toBeLessThanOrEqual(2);
|
||||||
|
expect(Math.abs(box.width - associatedItemColumns[index].width)).toBeLessThanOrEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.getByTestId("invoice-period-order-item-table-select-all-9001").check();
|
||||||
|
await expect(page.locator('[data-node-key="order_item:7701"]')).toHaveAttribute("aria-checked", "true");
|
||||||
|
await expect(page.locator('[data-node-key="order_item:7702"]')).toHaveAttribute("aria-checked", "true");
|
||||||
|
|
||||||
const orderWheelRoot = page.getByTestId("invoice-period-tree-action-wheel-order:9001");
|
const orderWheelRoot = page.getByTestId("invoice-period-tree-action-wheel-order:9001");
|
||||||
await orderWheelRoot.locator(".action-settings-wheel-trigger").click();
|
await orderWheelRoot.locator(".action-settings-wheel-trigger").click();
|
||||||
await expect(orderWheelRoot.getByTestId("action-settings-wheel-section-order")).toBeVisible();
|
await expect(orderWheelRoot.getByTestId("action-settings-wheel-section-order")).toBeVisible();
|
||||||
@@ -2234,6 +2477,17 @@ test.describe("Invoicing period tab", () => {
|
|||||||
});
|
});
|
||||||
expect(objectLineMetrics.mainHeight).toBeLessThanOrEqual(objectLineMetrics.mainLineHeight * 1.6);
|
expect(objectLineMetrics.mainHeight).toBeLessThanOrEqual(objectLineMetrics.mainLineHeight * 1.6);
|
||||||
expect(objectLineMetrics.subtitleHeight).toBeLessThanOrEqual(objectLineMetrics.subtitleLineHeight * 1.6);
|
expect(objectLineMetrics.subtitleHeight).toBeLessThanOrEqual(objectLineMetrics.subtitleLineHeight * 1.6);
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await expect(page.getByTestId("invoice-period-tree-toolbar")).toBeVisible();
|
||||||
|
await expect
|
||||||
|
.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth))
|
||||||
|
.toBe(true);
|
||||||
|
const itemScrollMetrics = await page.locator(".invoice-period-object-tree__scroll").evaluate((element) => ({
|
||||||
|
clientWidth: element.clientWidth,
|
||||||
|
scrollWidth: element.scrollWidth,
|
||||||
|
}));
|
||||||
|
expect(itemScrollMetrics.scrollWidth).toBeGreaterThan(itemScrollMetrics.clientWidth);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("@smoke period expanded order object tree omits table filters", async ({ page }) => {
|
test("@smoke period expanded order object tree omits table filters", async ({ page }) => {
|
||||||
@@ -2333,6 +2587,26 @@ test.describe("Invoicing period tab", () => {
|
|||||||
const multiCardBox = await getBoundingBox(multiAttributeCard, "multi-attribute customer card");
|
const multiCardBox = await getBoundingBox(multiAttributeCard, "multi-attribute customer card");
|
||||||
const singleCardBox = await getBoundingBox(singleAttributeCard, "single-attribute customer card");
|
const singleCardBox = await getBoundingBox(singleAttributeCard, "single-attribute customer card");
|
||||||
const stackBox = await getBoundingBox(attributeStack, "customer attribute stack");
|
const stackBox = await getBoundingBox(attributeStack, "customer attribute stack");
|
||||||
|
const identityBox = await getBoundingBox(
|
||||||
|
multiAttributeCard.locator(".period-customer-card__identity"),
|
||||||
|
"customer identity"
|
||||||
|
);
|
||||||
|
const selectorBox = await getBoundingBox(
|
||||||
|
multiAttributeCard.locator(".period-customer-card__selector"),
|
||||||
|
"customer selector"
|
||||||
|
);
|
||||||
|
const settingsBox = await getBoundingBox(
|
||||||
|
multiAttributeCard.locator(".period-customer-card__settings"),
|
||||||
|
"customer settings"
|
||||||
|
);
|
||||||
|
const countBox = await getBoundingBox(
|
||||||
|
multiAttributeCard.locator(".period-customer-card__count"),
|
||||||
|
"customer transaction count"
|
||||||
|
);
|
||||||
|
const amountBox = await getBoundingBox(
|
||||||
|
multiAttributeCard.locator(".period-customer-card__amount"),
|
||||||
|
"customer amount"
|
||||||
|
);
|
||||||
const tagBoxes = [];
|
const tagBoxes = [];
|
||||||
|
|
||||||
for (const [index, tagLocator] of tagLocators.entries()) {
|
for (const [index, tagLocator] of tagLocators.entries()) {
|
||||||
@@ -2346,6 +2620,12 @@ test.describe("Invoicing period tab", () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(Math.abs(multiCardBox.height - singleCardBox.height)).toBeLessThanOrEqual(3);
|
expect(Math.abs(multiCardBox.height - singleCardBox.height)).toBeLessThanOrEqual(3);
|
||||||
|
expect(identityBox.x).toBeLessThan(settingsBox.x);
|
||||||
|
expect(settingsBox.x + settingsBox.width).toBeLessThanOrEqual(multiCardBox.x + multiCardBox.width);
|
||||||
|
expect(Math.abs(countBox.y - amountBox.y)).toBeLessThanOrEqual(2);
|
||||||
|
expect(stackBox.y).toBeGreaterThan(countBox.y);
|
||||||
|
expect(Math.abs(stackBox.x - selectorBox.x)).toBeLessThanOrEqual(2);
|
||||||
|
expect(stackBox.width).toBeGreaterThan(multiCardBox.width * 0.8);
|
||||||
|
|
||||||
tagBoxes.slice(1).forEach((tagBox, index) => {
|
tagBoxes.slice(1).forEach((tagBox, index) => {
|
||||||
expect(Math.abs(tagBox.x - tagBoxes[0].x)).toBeLessThanOrEqual(1);
|
expect(Math.abs(tagBox.x - tagBoxes[0].x)).toBeLessThanOrEqual(1);
|
||||||
|
|||||||
@@ -27,6 +27,24 @@ vi.mock("buefy", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe("BuefyTree checkbox selection", () => {
|
describe("BuefyTree checkbox selection", () => {
|
||||||
|
it("removes the icon wrapper only for nodes that explicitly opt out", () => {
|
||||||
|
const wrapper = mount(BuefyTree, {
|
||||||
|
props: {
|
||||||
|
data: [
|
||||||
|
{ id: "default-icon", label: "Default icon", isLeaf: true },
|
||||||
|
{ id: "compact", label: "Compact", isLeaf: true, hideIcon: true, hideToggle: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-node-key="default-icon"] > .b-tree-node-content .b-tree-node-icon').exists()).toBe(true);
|
||||||
|
expect(wrapper.find('[data-node-key="compact"] > .b-tree-node-content .b-tree-node-icon').exists()).toBe(false);
|
||||||
|
expect(wrapper.find('[data-node-key="default-icon"] > .b-tree-node-content .b-tree-node-toggle').exists()).toBe(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(wrapper.find('[data-node-key="compact"] > .b-tree-node-content .b-tree-node-toggle').exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it("uses non-selectable checkable categories as lazy select-all branch controllers", async () => {
|
it("uses non-selectable checkable categories as lazy select-all branch controllers", async () => {
|
||||||
const load = vi.fn(async () => [
|
const load = vi.fn(async () => [
|
||||||
{ id: "order:70128", label: "Ordre #70128", selectable: true, isLeaf: true },
|
{ id: "order:70128", label: "Ordre #70128", selectable: true, isLeaf: true },
|
||||||
|
|||||||
@@ -111,6 +111,10 @@ vi.mock("vue-i18n", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/components/pagination/departmentTabs.vue", () => ({
|
||||||
|
getDepartmentName: (id) => (Number(id) === 75 ? "Department 75" : String(id)),
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@/components/displays/buttons/EditableTableColumn.vue", () => ({
|
vi.mock("@/components/displays/buttons/EditableTableColumn.vue", () => ({
|
||||||
default: {
|
default: {
|
||||||
name: "EditableTableColumn",
|
name: "EditableTableColumn",
|
||||||
@@ -381,7 +385,27 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("auto-expands order items when an order node is expanded", async () => {
|
it("refreshes a legacy collection date label when an existing order date changes", async () => {
|
||||||
|
const firstOrder = {
|
||||||
|
...mocks.order,
|
||||||
|
created_at: "2026-06-01 10:00:00",
|
||||||
|
booked: true,
|
||||||
|
};
|
||||||
|
const wrapper = mountTree({ transactions: [firstOrder] });
|
||||||
|
const collectionLabel = () =>
|
||||||
|
wrapper.get('[data-node-key="collected_order_invoice:3001"] .invoice-period-tree-node__label-text');
|
||||||
|
|
||||||
|
expect(collectionLabel().text()).toContain("1 Juni");
|
||||||
|
|
||||||
|
await wrapper.setProps({
|
||||||
|
transactions: [{ ...firstOrder, created_at: "2026-06-30 10:00:00" }],
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(collectionLabel().text()).toContain("30 Juni");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows order items directly when an order node is expanded", async () => {
|
||||||
const wrapper = mountTree();
|
const wrapper = mountTree();
|
||||||
|
|
||||||
await expandNode(wrapper, "collected_order_invoice:3001");
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
||||||
@@ -389,11 +413,14 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
await expandNode(wrapper, "order:9001");
|
await expandNode(wrapper, "order:9001");
|
||||||
|
|
||||||
expect(mocks.request).toHaveBeenCalledWith("/order/items", "GET", { order_id: 9001 });
|
expect(mocks.request).toHaveBeenCalledWith("/order/items", "GET", { order_id: 9001 });
|
||||||
expect(wrapper.find('[data-node-key="category:9001:order_items"]').classes()).toContain("is-expanded");
|
expect(wrapper.find('[data-node-key="category:9001:order_items"]').exists()).toBe(false);
|
||||||
|
expect(
|
||||||
|
wrapper.find('[data-node-key="order:9001"] > .b-tree-children > [data-node-key="order_item:501"]').exists()
|
||||||
|
).toBe(true);
|
||||||
expect(wrapper.text()).toContain("Premium wash");
|
expect(wrapper.text()).toContain("Premium wash");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows order-item quantity and price once through the standard editable fields", async () => {
|
it("uses one shared table header instead of repeating order-item field labels", async () => {
|
||||||
const wrapper = mountTree();
|
const wrapper = mountTree();
|
||||||
|
|
||||||
await expandNode(wrapper, "collected_order_invoice:3001");
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
||||||
@@ -405,12 +432,22 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
|
|
||||||
const quantity = wrapper.get('[data-testid="invoice-period-tree-field-order_item:501-quantity"]');
|
const quantity = wrapper.get('[data-testid="invoice-period-tree-field-order_item:501-quantity"]');
|
||||||
const price = wrapper.get('[data-testid="invoice-period-tree-field-order_item:501-price"]');
|
const price = wrapper.get('[data-testid="invoice-period-tree-field-order_item:501-price"]');
|
||||||
expect(quantity.text()).toContain("Antal");
|
const header = wrapper.get('[data-testid="invoice-period-order-item-table-header-9001"]');
|
||||||
expect(quantity.text()).toContain("2");
|
expect(header.text()).toContain("Produkt");
|
||||||
expect(price.text()).toContain("Pris");
|
expect(header.text()).toContain("Antal");
|
||||||
expect(price.text()).toContain("120 DKK");
|
expect(header.text()).toContain("Pris");
|
||||||
|
expect(quantity.get(".invoice-period-order-item-table__value").text()).toBe("2");
|
||||||
|
expect(price.get(".invoice-period-order-item-table__value").text()).toBe("120 DKK");
|
||||||
|
expect(quantity.find(".invoice-period-tree-node-field__label").exists()).toBe(false);
|
||||||
|
expect(price.find(".invoice-period-tree-node-field__label").exists()).toBe(false);
|
||||||
|
expect(quantity.get(".is-sr-only").text()).toBe("Antal:");
|
||||||
|
expect(price.get(".is-sr-only").text()).toBe("Pris:");
|
||||||
expect(quantity.find(".editable-table-column-stub").exists()).toBe(true);
|
expect(quantity.find(".editable-table-column-stub").exists()).toBe(true);
|
||||||
expect(price.find(".editable-table-column-stub").exists()).toBe(true);
|
expect(price.find(".editable-table-column-stub").exists()).toBe(true);
|
||||||
|
|
||||||
|
await wrapper.get('[data-testid="invoice-period-order-item-table-select-all-9001"]').setValue(true);
|
||||||
|
await flushPromises();
|
||||||
|
expect(wrapper.get('[data-node-key="order_item:501"]').attributes("aria-checked")).toBe("true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves untouched snapshot item values during an inline edit", async () => {
|
it("preserves untouched snapshot item values during an inline edit", async () => {
|
||||||
@@ -429,7 +466,6 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
await expandNode(wrapper, "collected_order_invoice:3001");
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
||||||
await expandNode(wrapper, "category:3001:collection_orders");
|
await expandNode(wrapper, "category:3001:collection_orders");
|
||||||
await expandNode(wrapper, "order:9001");
|
await expandNode(wrapper, "order:9001");
|
||||||
await expandNode(wrapper, "category:9001:order_items");
|
|
||||||
await wrapper.get('[data-testid="invoice-period-tree-field-editor-order_item:501-price"]').trigger("click");
|
await wrapper.get('[data-testid="invoice-period-tree-field-editor-order_item:501-price"]').trigger("click");
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -457,7 +493,6 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
await expandNode(wrapper, "collected_order_invoice:3001");
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
||||||
await expandNode(wrapper, "category:3001:collection_orders");
|
await expandNode(wrapper, "category:3001:collection_orders");
|
||||||
await expandNode(wrapper, "order:9001");
|
await expandNode(wrapper, "order:9001");
|
||||||
await expandNode(wrapper, "category:9001:order_items");
|
|
||||||
await wrapper.get('[data-testid="invoice-period-tree-field-editor-order_item:501-price"]').trigger("click");
|
await wrapper.get('[data-testid="invoice-period-tree-field-editor-order_item:501-price"]').trigger("click");
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -484,7 +519,6 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
await expandNode(wrapper, "collected_order_invoice:3001");
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
||||||
await expandNode(wrapper, "category:3001:collection_orders");
|
await expandNode(wrapper, "category:3001:collection_orders");
|
||||||
await expandNode(wrapper, "order:9001");
|
await expandNode(wrapper, "order:9001");
|
||||||
await expandNode(wrapper, "category:9001:order_items");
|
|
||||||
await wrapper.get('[data-testid="invoice-period-tree-field-editor-order_item:501-price"]').trigger("click");
|
await wrapper.get('[data-testid="invoice-period-tree-field-editor-order_item:501-price"]').trigger("click");
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
@@ -513,7 +547,8 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
expect(wrapper.get('[data-node-key="collected_order_invoice:3001"]').classes()).toContain("is-expanded");
|
expect(wrapper.get('[data-node-key="collected_order_invoice:3001"]').classes()).toContain("is-expanded");
|
||||||
expect(wrapper.get('[data-node-key="category:3001:collection_orders"]').classes()).toContain("is-expanded");
|
expect(wrapper.get('[data-node-key="category:3001:collection_orders"]').classes()).toContain("is-expanded");
|
||||||
expect(wrapper.get('[data-node-key="order:9001"]').classes()).toContain("is-expanded");
|
expect(wrapper.get('[data-node-key="order:9001"]').classes()).toContain("is-expanded");
|
||||||
expect(wrapper.get('[data-node-key="category:9001:order_items"]').classes()).toContain("is-expanded");
|
expect(wrapper.find('[data-node-key="category:9001:order_items"]').exists()).toBe(false);
|
||||||
|
expect(wrapper.get('[data-node-key="order_item:501"]').exists()).toBe(true);
|
||||||
expect(toggle().text()).toContain("Fold alle sammen");
|
expect(toggle().text()).toContain("Fold alle sammen");
|
||||||
expect(toggle().attributes("aria-pressed")).toBe("true");
|
expect(toggle().attributes("aria-pressed")).toBe("true");
|
||||||
|
|
||||||
@@ -536,7 +571,8 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
|
|
||||||
expect(wrapper.get('[data-testid="invoice-period-tree-toggle-all"]').text()).toContain("Fold alle sammen");
|
expect(wrapper.get('[data-testid="invoice-period-tree-toggle-all"]').text()).toContain("Fold alle sammen");
|
||||||
expect(wrapper.get('[data-node-key="order:9001"]').classes()).toContain("is-expanded");
|
expect(wrapper.get('[data-node-key="order:9001"]').classes()).toContain("is-expanded");
|
||||||
expect(wrapper.get('[data-node-key="category:9001:order_items"]').classes()).toContain("is-expanded");
|
expect(wrapper.find('[data-node-key="category:9001:order_items"]').exists()).toBe(false);
|
||||||
|
expect(wrapper.get('[data-node-key="order_item:501"]').exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders row action wheels for actionable collection, order, and order item nodes", async () => {
|
it("renders row action wheels for actionable collection, order, and order item nodes", async () => {
|
||||||
@@ -580,7 +616,7 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
expect(wrapper.find('[data-testid^="invoice-period-tree-field-empty-toggle-"]').exists()).toBe(false);
|
expect(wrapper.find('[data-testid^="invoice-period-tree-field-empty-toggle-"]').exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses wash labels, nests related order items, and omits product ids and type tags", async () => {
|
it("uses wash labels and aligns associated order items as direct table rows", async () => {
|
||||||
mocks.orderItems.push({
|
mocks.orderItems.push({
|
||||||
id: 502,
|
id: 502,
|
||||||
order_id: 9001,
|
order_id: 9001,
|
||||||
@@ -595,10 +631,23 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
await expandNode(wrapper, "collected_order_invoice:3001");
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
||||||
await expandNode(wrapper, "category:3001:collection_orders");
|
await expandNode(wrapper, "category:3001:collection_orders");
|
||||||
await expandNode(wrapper, "order:9001");
|
await expandNode(wrapper, "order:9001");
|
||||||
await expandNode(wrapper, "order_item:501");
|
|
||||||
|
|
||||||
expect(wrapper.get('[data-node-key="order:9001"]').text()).toContain("Vask #9001");
|
expect(wrapper.get('[data-node-key="order:9001"]').text()).toContain("Vask #9001");
|
||||||
expect(wrapper.find('[data-node-key="order_item:501"] [data-node-key="order_item:502"]').exists()).toBe(true);
|
expect(
|
||||||
|
wrapper.find('[data-node-key="order:9001"] > .b-tree-children > [data-node-key="order_item:502"]').exists()
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.get('[data-testid="invoice-period-order-item-table-row-502"] .invoice-period-order-item-table__relation')
|
||||||
|
.text()
|
||||||
|
).toBe("+");
|
||||||
|
expect(wrapper.get('[data-testid="invoice-period-order-item-table-row-502"] .is-sr-only').text()).toContain(
|
||||||
|
"Premium wash"
|
||||||
|
);
|
||||||
|
expect(wrapper.findAll('[data-testid="invoice-period-order-item-table-header-9001"]')).toHaveLength(1);
|
||||||
|
expect(wrapper.get('[data-testid="invoice-period-tree-field-order_item:502-reference"] .is-sr-only').text()).toBe(
|
||||||
|
"Reference:"
|
||||||
|
);
|
||||||
expect(wrapper.text()).toContain("Interior wash with a complete descriptive product label");
|
expect(wrapper.text()).toContain("Interior wash with a complete descriptive product label");
|
||||||
expect(wrapper.find('[data-testid$="-product_id"]').exists()).toBe(false);
|
expect(wrapper.find('[data-testid$="-product_id"]').exists()).toBe(false);
|
||||||
expect(wrapper.find(".tag.is-white").exists()).toBe(false);
|
expect(wrapper.find(".tag.is-white").exists()).toBe(false);
|
||||||
@@ -791,7 +840,7 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
expect(wrapper.text()).not.toContain("E-conomic kladde #88");
|
expect(wrapper.text()).not.toContain("E-conomic kladde #88");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("colors collection, invoice, order, and item icons from the booked state", async () => {
|
it("colors structural icons from the booked state and omits the redundant order-item icon", async () => {
|
||||||
const wrapper = mountTree();
|
const wrapper = mountTree();
|
||||||
|
|
||||||
await expandNode(wrapper, "collected_order_invoice:3001");
|
await expandNode(wrapper, "collected_order_invoice:3001");
|
||||||
@@ -816,12 +865,13 @@ describe("InvoicingPeriodObjectTree", () => {
|
|||||||
expect(
|
expect(
|
||||||
wrapper.get('[data-node-key="order:9001"] > .b-tree-node-content .b-tree-node-icon span').classes()
|
wrapper.get('[data-node-key="order:9001"] > .b-tree-node-content .b-tree-node-icon span').classes()
|
||||||
).toContain("has-text-success");
|
).toContain("has-text-success");
|
||||||
expect(
|
expect(wrapper.find('[data-node-key="category:9001:order_items"]').exists()).toBe(false);
|
||||||
wrapper.get('[data-node-key="category:9001:order_items"] > .b-tree-node-content .b-tree-node-icon span').classes()
|
expect(wrapper.find('[data-node-key="order_item:501"] > .b-tree-node-content .b-tree-node-icon').exists()).toBe(
|
||||||
).toContain("has-text-success");
|
false
|
||||||
expect(
|
);
|
||||||
wrapper.get('[data-node-key="order_item:501"] > .b-tree-node-content .b-tree-node-icon span').classes()
|
expect(wrapper.find('[data-node-key="order_item:501"] > .b-tree-node-content .b-tree-node-toggle').exists()).toBe(
|
||||||
).toContain("has-text-success");
|
false
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("downloads booked e-conomic invoice pdfs for booked invoice nodes", async () => {
|
it("downloads booked e-conomic invoice pdfs for booked invoice nodes", async () => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
TREE_NODE_TYPES,
|
TREE_NODE_TYPES,
|
||||||
buildRelativeCollectionLabel,
|
buildRelativeCollectionLabel,
|
||||||
buildCollectionRootNodes,
|
buildCollectionRootNodes,
|
||||||
|
buildOrderItemTableRows,
|
||||||
buildOrderItemTree,
|
buildOrderItemTree,
|
||||||
classifyAttachment,
|
classifyAttachment,
|
||||||
getTreeAmount,
|
getTreeAmount,
|
||||||
@@ -66,7 +67,7 @@ describe("invoicing period tree node builders", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses relative collection labels for whole-month and multi-collection ranges", () => {
|
it("uses order date ranges for both whole-month and multi-collection views", () => {
|
||||||
const wholeMonthNodes = buildCollectionRootNodes(
|
const wholeMonthNodes = buildCollectionRootNodes(
|
||||||
{ customer_number: 2001, customer_name: "ACME" },
|
{ customer_number: 2001, customer_name: "ACME" },
|
||||||
[
|
[
|
||||||
@@ -82,10 +83,11 @@ describe("invoicing period tree node builders", () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wholeMonthNodes[0].label).toBe("Juni");
|
expect(wholeMonthNodes[0].label).toBe("2 Juni → 18 Juni");
|
||||||
expect(wholeMonthNodes[0].meta.relativeLabel).toMatchObject({
|
expect(wholeMonthNodes[0].meta.relativeLabel).toMatchObject({
|
||||||
kind: "month",
|
kind: "range",
|
||||||
monthLabel: "Juni",
|
startLabel: "2 Juni",
|
||||||
|
endLabel: "18 Juni",
|
||||||
});
|
});
|
||||||
|
|
||||||
const rangeNodes = buildCollectionRootNodes(
|
const rangeNodes = buildCollectionRootNodes(
|
||||||
@@ -304,7 +306,13 @@ describe("invoicing period tree node builders", () => {
|
|||||||
washId: "W-12",
|
washId: "W-12",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(itemNode).toMatchObject({ type: TREE_NODE_TYPES.ORDER_ITEM, isLeaf: true, actionable: true });
|
expect(itemNode).toMatchObject({
|
||||||
|
type: TREE_NODE_TYPES.ORDER_ITEM,
|
||||||
|
isLeaf: true,
|
||||||
|
actionable: true,
|
||||||
|
hideToggle: true,
|
||||||
|
hideIcon: true,
|
||||||
|
});
|
||||||
expect(attachmentNode).toMatchObject({
|
expect(attachmentNode).toMatchObject({
|
||||||
type: TREE_NODE_TYPES.ATTACHMENT,
|
type: TREE_NODE_TYPES.ATTACHMENT,
|
||||||
meta: { attachmentId: 8, orderId: 91, attachmentType: "image" },
|
meta: { attachmentId: 8, orderId: 91, attachmentType: "image" },
|
||||||
@@ -347,6 +355,25 @@ describe("invoicing period tree node builders", () => {
|
|||||||
expect(nodes.slice(1).every((node) => node.isLeaf)).toBe(true);
|
expect(nodes.slice(1).every((node) => node.isLeaf)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("flattens associated order items into stable direct table rows with one shared header", () => {
|
||||||
|
const rows = buildOrderItemTableRows(
|
||||||
|
[
|
||||||
|
{ id: 501, product_name: "Wash" },
|
||||||
|
{ id: 502, related_item_id: 501, product_name: "Interior" },
|
||||||
|
{ id: 503, related_item_id: 502, product_name: "Mat cleaning" },
|
||||||
|
{ id: 504, related_item_id: 999, product_name: "Orphan" },
|
||||||
|
{ id: 505, related_item_id: 506, product_name: "Cycle A" },
|
||||||
|
{ id: 506, related_item_id: 505, product_name: "Cycle B" },
|
||||||
|
],
|
||||||
|
{ orderId: 91 }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(rows.map((node) => node.meta.itemId)).toEqual([501, 502, 503, 504, 505, 506]);
|
||||||
|
expect(rows.map((node) => node.meta.tableDepth)).toEqual([0, 1, 2, 0, 0, 0]);
|
||||||
|
expect(rows.map((node) => node.meta.showTableHeader)).toEqual([true, false, false, false, false, false]);
|
||||||
|
expect(rows.every((node) => node.isLeaf && node.children.length === 0 && node.meta.orderId === 91)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("creates economic invoice nodes with draft/booked metadata", () => {
|
it("creates economic invoice nodes with draft/booked metadata", () => {
|
||||||
const node = makeEconomicInvoiceNode({
|
const node = makeEconomicInvoiceNode({
|
||||||
collectionId: 3001,
|
collectionId: 3001,
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ const payload = (revision = "rev-1") => ({
|
|||||||
invoice_collection_id: 3001,
|
invoice_collection_id: 3001,
|
||||||
in_selected_period: true,
|
in_selected_period: true,
|
||||||
total_net_amount: 200,
|
total_net_amount: 200,
|
||||||
items: [{ id: 501, order_id: 9001, product_name: "Wash", quantity: 1, price: 200 }],
|
items: [
|
||||||
|
{ id: 501, product_name: "Wash", quantity: 1, price: 200 },
|
||||||
|
{ id: 502, related_item_id: 501, product_name: "Interior", quantity: 1, price: 25 },
|
||||||
|
],
|
||||||
attachments: [],
|
attachments: [],
|
||||||
bookings: [],
|
bookings: [],
|
||||||
xlvask: [],
|
xlvask: [],
|
||||||
@@ -79,6 +82,25 @@ const labels = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("invoicing period selected-customer tree snapshot", () => {
|
describe("invoicing period selected-customer tree snapshot", () => {
|
||||||
|
it("keeps date-range collection labels when building the complete snapshot tree", () => {
|
||||||
|
const rangePayload = payload("rev-range");
|
||||||
|
rangePayload.date_from = "2026-01-01";
|
||||||
|
rangePayload.date_to = "2026-01-31";
|
||||||
|
rangePayload.collections[0].orders[0].created_at = "2026-01-01 10:00:00";
|
||||||
|
rangePayload.collections[0].orders[1].created_at = "2026-01-31 10:00:00";
|
||||||
|
rangePayload.collections[0].orders[1].in_selected_period = true;
|
||||||
|
|
||||||
|
const snapshot = normalizeInvoicingPeriodTreeSnapshot({ data: { data: rangePayload } });
|
||||||
|
const roots = buildCompleteSnapshotRootNodes(snapshot, { ...labels, locale: "da-DK" });
|
||||||
|
|
||||||
|
expect(roots[0].label).toBe("1 Januar → 31 Januar");
|
||||||
|
expect(roots[0].meta.relativeLabel).toMatchObject({
|
||||||
|
kind: "range",
|
||||||
|
startLabel: "1 Januar",
|
||||||
|
endLabel: "31 Januar",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("normalizes a complete capability-gated snapshot and keeps off-period orders in the complete collection", () => {
|
it("normalizes a complete capability-gated snapshot and keeps off-period orders in the complete collection", () => {
|
||||||
const snapshot = normalizeInvoicingPeriodTreeSnapshot({ data: { data: payload() } });
|
const snapshot = normalizeInvoicingPeriodTreeSnapshot({ data: { data: payload() } });
|
||||||
expect(snapshot).toMatchObject({
|
expect(snapshot).toMatchObject({
|
||||||
@@ -101,7 +123,15 @@ describe("invoicing period selected-customer tree snapshot", () => {
|
|||||||
const orderNodes = roots[0].children[0].children;
|
const orderNodes = roots[0].children[0].children;
|
||||||
expect(orderNodes.map((node) => node.id)).toEqual(["order:9001", "order:9002"]);
|
expect(orderNodes.map((node) => node.id)).toEqual(["order:9001", "order:9002"]);
|
||||||
expect(orderNodes[1].meta.inSelectedPeriod).toBe(false);
|
expect(orderNodes[1].meta.inSelectedPeriod).toBe(false);
|
||||||
expect(orderNodes[0].children[0].children[0].id).toBe("order_item:501");
|
expect(orderNodes[0].children[0].id).toBe("order_item:501");
|
||||||
|
expect(orderNodes[0].children[0].meta.showTableHeader).toBe(true);
|
||||||
|
expect(orderNodes[0].children[0].meta.orderId).toBe(9001);
|
||||||
|
expect(orderNodes[0].children[1]).toMatchObject({
|
||||||
|
id: "order_item:502",
|
||||||
|
isLeaf: true,
|
||||||
|
meta: { orderId: 9001, tableDepth: 1, showTableHeader: false },
|
||||||
|
});
|
||||||
|
expect(orderNodes[0].children.some((node) => node.category === "order_items")).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects incomplete or capability-disabled payloads", () => {
|
it("rejects incomplete or capability-disabled payloads", () => {
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ const periodObjectTreeSource = readFileSync(
|
|||||||
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue"),
|
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue"),
|
||||||
"utf8"
|
"utf8"
|
||||||
);
|
);
|
||||||
|
const periodFlagListSource = readFileSync(
|
||||||
|
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue"),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
const periodTreeSnapshotSource = readFileSync(
|
const periodTreeSnapshotSource = readFileSync(
|
||||||
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTreeSnapshot.ts"),
|
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/services/invoicingPeriodTreeSnapshot.ts"),
|
||||||
"utf8"
|
"utf8"
|
||||||
@@ -440,6 +444,9 @@ describe("Periode tab contract", () => {
|
|||||||
|
|
||||||
it("stacks grouped selectors in the desktop rail and keeps compact navigation responsive", () => {
|
it("stacks grouped selectors in the desktop rail and keeps compact navigation responsive", () => {
|
||||||
expect(periodRightSource).toContain("grid-template-columns: minmax(0, 1fr);");
|
expect(periodRightSource).toContain("grid-template-columns: minmax(0, 1fr);");
|
||||||
|
expect(periodRightSource).toContain('class="period-selector-row"');
|
||||||
|
expect(periodRightSource).toContain("grid-template-columns: minmax(0, 1fr) 72px 1rem;");
|
||||||
|
expect(periodRightSource).toContain("min-height: 4rem;");
|
||||||
expect(periodRightSource).toContain("@media screen and (min-width: 601px) and (max-width: 1023px)");
|
expect(periodRightSource).toContain("@media screen and (min-width: 601px) and (max-width: 1023px)");
|
||||||
expect(periodRightSource).toContain("grid-template-columns: repeat(2, minmax(0, 1fr));");
|
expect(periodRightSource).toContain("grid-template-columns: repeat(2, minmax(0, 1fr));");
|
||||||
expect(periodRightSource).toContain("@media screen and (max-width: 600px)");
|
expect(periodRightSource).toContain("@media screen and (max-width: 600px)");
|
||||||
@@ -453,6 +460,16 @@ describe("Periode tab contract", () => {
|
|||||||
expect(periodViewAllSource).toContain("@container (max-width: 1024px)");
|
expect(periodViewAllSource).toContain("@container (max-width: 1024px)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps customer card content and actions on stable grid rows", () => {
|
||||||
|
expect(periodViewAllSource).toContain('"count amount amount"');
|
||||||
|
expect(periodViewAllSource).toContain('"attributes attributes attributes"');
|
||||||
|
expect(periodViewAllSource).toContain("grid-template-columns: 1.25rem auto minmax(0, 1fr);");
|
||||||
|
expect(periodFlagListSource).toContain("grid-template-columns: 1.2rem minmax(0, 1fr);");
|
||||||
|
expect(periodFlagListSource).toContain("grid-column: 2;");
|
||||||
|
expect(periodFlagListSource).toContain("max-height: 2.5rem;");
|
||||||
|
expect(periodFlagListSource).toContain(".invoice-period-flag-row:focus-within");
|
||||||
|
});
|
||||||
|
|
||||||
it("sizes the inline month selector to the available content width with a readable cap", () => {
|
it("sizes the inline month selector to the available content width with a readable cap", () => {
|
||||||
expect(periodViewHomeSource).toContain('class="invoicing-period-month-entry__selector"');
|
expect(periodViewHomeSource).toContain('class="invoicing-period-month-entry__selector"');
|
||||||
expect(periodViewHomeSource).toContain("max-width: 32rem;");
|
expect(periodViewHomeSource).toContain("max-width: 32rem;");
|
||||||
@@ -738,13 +755,29 @@ describe("Periode tab contract", () => {
|
|||||||
expect(periodObjectTreeSource).toContain("overflow-wrap: anywhere;");
|
expect(periodObjectTreeSource).toContain("overflow-wrap: anywhere;");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("switches narrow object-tree metadata to a consistent four-column grid", () => {
|
||||||
|
expect(periodObjectTreeSource).toContain("@container (max-width: 56rem)");
|
||||||
|
expect(periodObjectTreeSource).toContain("grid-template-columns: repeat(4, minmax(0, 1fr));");
|
||||||
|
expect(periodObjectTreeSource).toContain("`invoice-period-tree-node-field--${field.key}`");
|
||||||
|
expect(periodObjectTreeSource).toContain(".invoice-period-tree-node-field--total_net_amount");
|
||||||
|
expect(periodObjectTreeSource).toContain("grid-column: 4 !important;");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the order department in the object-tree metadata", () => {
|
||||||
|
expect(periodObjectTreeSource).toContain('key: "department"');
|
||||||
|
expect(periodObjectTreeSource).toContain('label: commonText("department", "Afdeling")');
|
||||||
|
expect(periodObjectTreeSource).toContain("const orderDepartmentLabel = (order: any) =>");
|
||||||
|
expect(periodObjectTreeSource).toContain("order?.department_name || order?.department?.name");
|
||||||
|
expect(periodObjectTreeSource).toContain("getDepartmentName(departmentId)");
|
||||||
|
expect(periodObjectTreeSource).not.toContain("object.cashier_name ||");
|
||||||
|
});
|
||||||
|
|
||||||
it("exposes object-specific lazy child categories and multi-select operations", () => {
|
it("exposes object-specific lazy child categories and multi-select operations", () => {
|
||||||
[
|
[
|
||||||
"COLLECTION_ORDERS",
|
"COLLECTION_ORDERS",
|
||||||
"COLLECTION_AGREEMENTS",
|
"COLLECTION_AGREEMENTS",
|
||||||
"COLLECTION_PAYMENTS",
|
"COLLECTION_PAYMENTS",
|
||||||
"COLLECTION_ECONOMIC",
|
"COLLECTION_ECONOMIC",
|
||||||
"ORDER_ITEMS",
|
|
||||||
"ORDER_ATTACHMENTS_CERTIFICATES",
|
"ORDER_ATTACHMENTS_CERTIFICATES",
|
||||||
"ORDER_ATTACHMENTS_IMAGES",
|
"ORDER_ATTACHMENTS_IMAGES",
|
||||||
"ORDER_ATTACHMENTS_OTHER",
|
"ORDER_ATTACHMENTS_OTHER",
|
||||||
@@ -755,6 +788,8 @@ describe("Periode tab contract", () => {
|
|||||||
].forEach((category) => {
|
].forEach((category) => {
|
||||||
expect(periodTreeNodeServiceSource).toContain(category);
|
expect(periodTreeNodeServiceSource).toContain(category);
|
||||||
});
|
});
|
||||||
|
expect(periodObjectTreeSource).toContain("buildOrderItemTableRows");
|
||||||
|
expect(periodObjectTreeSource).not.toContain("TREE_CATEGORY_TYPES.ORDER_ITEMS");
|
||||||
|
|
||||||
[
|
[
|
||||||
"runCollectionBulkAction",
|
"runCollectionBulkAction",
|
||||||
|
|||||||
Reference in New Issue
Block a user