Add invoice collection bulk actions and table header preferences functionality

This commit is contained in:
Jeppe Bundgaard
2026-07-08 15:43:14 +02:00
parent d1d7b72441
commit 042e477252
31 changed files with 1385 additions and 358 deletions
+19 -1
View File
@@ -49,8 +49,26 @@ textarea.has-sharp-edges {
--bulma-card-shadow: none !important;
--bulma-skeleton-background: hsla(197 100% 35% / 0.70) !important;
--pleno-compact-table-header-font-size: 0.72rem;
--pleno-compact-table-header-line-height: 1;
}
body:not(.pleno-large-table-headers) .table thead th {
font-size: var(--pleno-compact-table-header-font-size);
line-height: var(--pleno-compact-table-header-line-height);
white-space: nowrap;
vertical-align: middle;
}
.pleno-table-header-content {
display: inline-flex;
align-items: center;
gap: 0.15rem;
max-width: 100%;
vertical-align: middle;
}
/*:root {*/
/* --bulma-primary-h: 207deg !important;*/
/* --bulma-primary-s: 60% !important;*/
@@ -125,4 +143,4 @@ textarea.has-sharp-edges {
.mt-30-tablet {
margin-top: 30px !important;
}
}
}
@@ -0,0 +1,122 @@
<script setup>
import { computed } from "vue";
import { useI18n } from "vue-i18n";
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import ActionSettingsWheelItemLabel from "@/components/displays/buttons/ActionSettingsWheelItemLabel.vue";
import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
const props = defineProps({
selectedInvoiceCollectionIds: {
type: Array,
default: () => [],
},
totalInvoiceCollectionCount: {
type: Number,
default: 0,
},
allSelected: {
type: Boolean,
default: false,
},
allExpanded: {
type: Boolean,
default: false,
},
invoiceQueueBusy: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["invoiceSelected", "bulkAction", "toggleSelectAll", "toggleExpandAll"]);
const { t } = useI18n();
const selectedCount = computed(() => props.selectedInvoiceCollectionIds.length);
const hasSelectableCollections = computed(() => props.totalInvoiceCollectionCount > 0);
const hasSelectedCollections = computed(() => selectedCount.value > 0);
const hasMultipleSelectedCollections = computed(() => selectedCount.value > 1);
const triggerLabel = computed(() =>
t("invoicing_period.invoice_collection_actions.menu.label", { count: selectedCount.value })
);
const selectAllLabel = computed(() =>
props.allSelected
? t("invoicing_period.invoice_collection_actions.menu.unselect_all")
: t("invoicing_period.invoice_collection_actions.menu.select_all")
);
const expandAllLabel = computed(() =>
props.allExpanded
? t("invoicing_period.invoice_collection_actions.menu.collapse_all")
: t("invoicing_period.invoice_collection_actions.menu.expand_all")
);
const emitBulkAction = (action) => emit("bulkAction", action);
</script>
<template>
<div
v-if="hasSelectableCollections"
class="invoice-collection-selection-action-wheel"
data-testid="invoice-collection-selection-action-wheel"
>
<ActionSettingsWheelButton icon="fas fa-sliders-h" :label="triggerLabel">
<template #actions>
<ActionSettingsWheelItemLabel
:label="t('invoicing_period.invoice_collection_actions.menu.selection_section')"
data-testid="invoice-collection-selection-action-wheel-selection-section"
/>
<ActionSettingsWheelItem
icon="fas fa-check-double"
:label="selectAllLabel"
:click-action="() => emit('toggleSelectAll')"
test-id="invoice-collection-selection-action-wheel-select-all"
/>
<ActionSettingsWheelItem
:icon="allExpanded ? 'fas fa-compress-alt' : 'fas fa-expand-alt'"
:label="expandAllLabel"
:click-action="() => emit('toggleExpandAll')"
test-id="invoice-collection-selection-action-wheel-expand-all"
/>
<template v-if="hasSelectedCollections">
<ActionSettingsWheelItemLabel
:label="t('invoicing_period.invoice_collection_actions.menu.modification_section')"
data-testid="invoice-collection-selection-action-wheel-modification-section"
/>
<ActionSettingsWheelItem
icon="fas fa-file-invoice-dollar"
:label="t('invoicing_period.invoice_collection_actions.actions.queue_economic')"
:click-action="() => emit('invoiceSelected')"
:disabled="invoiceQueueBusy"
test-id="invoice-collection-selection-action-wheel-invoice-selected"
/>
<ActionSettingsWheelItem
icon="fas fa-broom"
:label="t('invoicing_period.invoice_collection_actions.actions.remove_customer_rule_violations')"
:click-action="() => emitBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES)"
test-id="invoice-collection-selection-action-wheel-clean-rules"
/>
<ActionSettingsWheelItem
v-if="hasMultipleSelectedCollections"
icon="fas fa-compress-arrows-alt"
:label="t('invoicing_period.invoice_collection_actions.actions.merge_collections')"
:click-action="() => emitBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.MERGE)"
test-id="invoice-collection-selection-action-wheel-merge"
/>
<ActionSettingsWheelItem
icon="fas fa-calendar-alt"
:label="t('invoicing_period.invoice_collection_actions.actions.split_by_month')"
:click-action="() => emitBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.SPLIT_BY_MONTH)"
test-id="invoice-collection-selection-action-wheel-split-by-month"
/>
<ActionSettingsWheelItem
icon="fas fa-undo"
:label="t('invoicing_period.invoice_collection_actions.actions.reset_hidden_item_prices')"
:click-action="() => emitBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.RESET_HIDDEN_PRICES)"
test-id="invoice-collection-selection-action-wheel-reset-hidden-prices"
/>
</template>
</template>
</ActionSettingsWheelButton>
</div>
</template>
@@ -0,0 +1,7 @@
export const INVOICE_COLLECTION_BULK_ACTIONS = Object.freeze({
CLEAN_CUSTOMER_RULES: "remove_customer_rule_violations",
MERGE: "merge_collections",
SPLIT_BY_MONTH: "split_by_month",
RESET_HIDDEN_PRICES: "reset_hidden_item_prices",
QUEUE_ECONOMIC: "queue_economic",
});
@@ -1,12 +1,17 @@
<script setup>
import { useI18n } from "vue-i18n";
import { BCheckbox } from "buefy";
const { t, locale } = useI18n();
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import InvoiceCollectionSelectionActionWheel from "@/components/displays/department/pos/orders/InvoiceCollectionSelectionActionWheel.vue";
import OrderAttachmentsActionButton from "@/components/displays/department/pos/orders/OrderAttachmentsActionButton.vue";
import AssignDraftOrderCustomerModal from "@/components/displays/modals/AssignDraftOrderCustomerModal.vue";
import InvoicingPeriodFlagList from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodFlagList.vue";
import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
import SuperuserInvoiceRowActions from "@/components/displays/superuser/tables/SuperuserInvoiceRowActions.vue";
import { useLargeTableHeaders } from "@/services/tableHeaderPreferences.js";
const props = defineProps({
orders: {
@@ -58,7 +63,6 @@ import { showPopper, removePopperIfOpen, popperBox } from "@/components/displays
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import { usePaginatedListInstance } from "@/components/pagination/paginatedList.vue";
const { orderBy, orderDirection, setOrder, loadList } = usePaginatedListInstance();
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import OrderContentTable from "@/components/displays/superuser/tables/OrderContentTable.vue";
import InvoiceMultipleCollectionsModal from "@/components/displays/modals/InvoiceMultipleCollectionsModal.vue";
import Swal from "sweetalert2";
@@ -266,14 +270,6 @@ const normalizeInvoiceCollectionId = (invoiceCollectionId) => {
: null;
};
const INVOICE_COLLECTION_BULK_ACTIONS = Object.freeze({
CLEAN_CUSTOMER_RULES: "remove_customer_rule_violations",
MERGE: "merge_collections",
SPLIT_BY_MONTH: "split_by_month",
RESET_HIDDEN_PRICES: "reset_hidden_item_prices",
QUEUE_ECONOMIC: "queue_economic",
});
const getApiPayload = (response) => response?.data?.data ?? response?.data ?? response ?? {};
const escapeHtml = (value) => String(value ?? "")
@@ -476,6 +472,18 @@ const toggleInvoiceCollectionSelection = (invoiceCollectionId) => {
}
};
const setInvoiceCollectionSelection = (invoiceCollectionId, checked) => {
const normalizedInvoiceCollectionId = normalizeInvoiceCollectionId(invoiceCollectionId);
if (normalizedInvoiceCollectionId === null) {
return;
}
const isSelected = selectedInvoiceCollections.value.includes(normalizedInvoiceCollectionId);
if ((checked && !isSelected) || (!checked && isSelected)) {
toggleInvoiceCollectionSelection(normalizedInvoiceCollectionId);
}
};
const isInvoiceCollectionSelected = (invoiceCollectionId) => {
const normalizedInvoiceCollectionId = normalizeInvoiceCollectionId(invoiceCollectionId);
if (normalizedInvoiceCollectionId === null) {
@@ -540,6 +548,7 @@ const isInvoiceQueueBusy = computed(() => {
const getSelectedInvoiceCollectionIds = () => selectedInvoiceCollections.value
.map((invoiceCollectionId) => normalizeInvoiceCollectionId(invoiceCollectionId))
.filter((invoiceCollectionId) => invoiceCollectionId !== null);
const selectableInvoiceCollectionCount = computed(() => getUniqueInvoiceCollections().length);
const getBulkActionLabel = (action) => t(`invoicing_period.invoice_collection_actions.actions.${action}`);
@@ -1106,70 +1115,17 @@ const formatCashierName = (order) => {
<thead>
<tr v-if="props.invoiceView && props.allowSelectMultiple">
<td colspan="100%">
<div class="buttons">
<!-- Invoice selected collections -->
<button
class="button is-small"
@click="invoiceSelectedCollections()"
:disabled="selectedInvoiceCollections.length === 0 || isInvoiceQueueBusy"
>
{{ $t("common.invoice") }} {{ $t("global.selected_multiple") }} ({{
selectedInvoiceCollections.length
}})
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.remove_customer_rule_violations") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.MERGE)"
:disabled="selectedInvoiceCollections.length < 2"
>
{{ t("invoicing_period.invoice_collection_actions.actions.merge_collections") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.SPLIT_BY_MONTH)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.split_by_month") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.RESET_HIDDEN_PRICES)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.reset_hidden_item_prices") }}
</button>
<!-- Select / Unselect all invoice collections -->
<button
class="button is-small"
@click="selectAllInvoiceCollections()"
:class="{
'is-light': !isInvoiceCollectionSelectedAll(),
'is-dark': isInvoiceCollectionSelectedAll(),
}"
>
{{ isInvoiceCollectionSelectedAll() ? $t("global.unselect") : $t("common.select") }}
{{ $t("common.all").toLowerCase() }}
</button>
<!-- Expand / Collapse all invoice collections -->
<button
class="button is-small"
@click="toggleAutoExpandAll()"
:class="{
'is-light': !isAutoExpandAll(),
'is-dark': isAutoExpandAll(),
}"
>
{{ isAutoExpandAll() ? $t("global.collapse") : $t("global.expand") }}
{{ $t("common.all").toLowerCase() }}
</button>
</div>
<InvoiceCollectionSelectionActionWheel
:selected-invoice-collection-ids="selectedInvoiceCollections"
:total-invoice-collection-count="selectableInvoiceCollectionCount"
:all-selected="isInvoiceCollectionSelectedAll()"
:all-expanded="isAutoExpandAll()"
:invoice-queue-busy="isInvoiceQueueBusy"
@invoice-selected="invoiceSelectedCollections"
@bulk-action="runInvoiceCollectionBulkAction"
@toggle-select-all="selectAllInvoiceCollections"
@toggle-expand-all="toggleAutoExpandAll"
/>
</td>
</tr>
<tr>
@@ -1181,12 +1137,15 @@ const formatCashierName = (order) => {
v-if="tableHeaders.sortable"
:class="{
'is-clickable': tableHeaders.sortable,
'has-text-centered': tableHeaders.name === 'id',
'is-narrow': ['id', 'reg_1', 'actions'].includes(tableHeaders.name),
'pos-orders-table-header--compact': !useLargeTableHeaders,
'pos-orders-table-id-header': tableHeaders.name === 'id',
'pos-orders-table-header--vehicles': tableHeaders.name === 'reg_1',
}"
@click="onTableHeaderClick(tableHeaders.name)"
>
<span class="is-flex-wrap-nowrap">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
<span class="is-flex-wrap-nowrap pleno-table-header-content">
<span class="has-text-weight-bold pos-orders-table-header__label">{{ tableHeaders.title }}</span>
<span
v-if="isColumnCurrentlyBeingSortedBy(tableHeaders.name)"
:class="{
@@ -1200,11 +1159,14 @@ const formatCashierName = (order) => {
<th
v-else
:class="{
'has-text-centered': tableHeaders.name === 'id',
'is-narrow': ['id', 'reg_1', 'actions'].includes(tableHeaders.name),
'pos-orders-table-header--compact': !useLargeTableHeaders,
'pos-orders-table-id-header': tableHeaders.name === 'id',
'pos-orders-table-header--vehicles': tableHeaders.name === 'reg_1',
}"
>
<span class="is-flex-wrap-nowrap">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
<span class="is-flex-wrap-nowrap pleno-table-header-content">
<span class="has-text-weight-bold pos-orders-table-header__label">{{ tableHeaders.title }}</span>
</span>
</th>
</template>
@@ -1257,9 +1219,11 @@ const formatCashierName = (order) => {
</td>
<!-- Actions for the invoice collection -->
<td class="is-narrow">
<ActionSettingsWheelButton v-bind:invoice_collection_id="order.invoice_collection_id">
<template #actions> </template>
</ActionSettingsWheelButton>
<SuperuserInvoiceRowActions :include-buffer="true">
<ActionSettingsWheelButton v-bind:invoice_collection_id="order.invoice_collection_id">
<template #actions> </template>
</ActionSettingsWheelButton>
</SuperuserInvoiceRowActions>
</td>
</tr>
<!-- Warning, if there's orders in the collection that are filtered out -->
@@ -1295,23 +1259,21 @@ const formatCashierName = (order) => {
</td>
</tr>
</template>
<tr v-if="isObjectVisible(order)" :class="{ 'has-background-info-light': isSystemOrder(order) }">
<tr
v-if="isObjectVisible(order)"
class="pos-orders-table-row"
:class="{ 'has-background-info-light': isSystemOrder(order) }"
>
<template v-if="props.invoiceView && props.allowSelectMultiple">
<td class="is-narrow">
<button
class="button is-small"
@click="toggleInvoiceCollectionSelection(order.invoice_collection_id)"
:class="{
'is-light': !isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)),
'is-dark': isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)),
}"
>
{{
isInvoiceCollectionSelected(parseInt(order.invoice_collection_id))
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
</button>
<td class="is-narrow pos-order-invoice-collection-selector-cell">
<BCheckbox
class="pos-order-invoice-collection-selector"
:model-value="isInvoiceCollectionSelected(order.invoice_collection_id)"
:disabled="normalizeInvoiceCollectionId(order.invoice_collection_id) === null"
:aria-label="`${SessionUser.objects.global.language.select} ${SessionUser.objects.global.language.invoice.toLowerCase()} #${order.invoice_collection_id || order.id}`"
:data-testid="`pos-order-invoice-collection-selector-${order.id}`"
@update:model-value="(checked) => setInvoiceCollectionSelection(order.invoice_collection_id, checked)"
/>
</td>
</template>
<template v-if="props.invoiceView">
@@ -1332,52 +1294,53 @@ const formatCashierName = (order) => {
</button>
</td>
</template>
<td>
<ColorIndicator
v-bind:color_class="getOrderIndicatorColorClass(order)"
v-bind:icon_class="getOrderIndicatorIconClass(order)"
v-bind:is_narrow="true"
v-bind:label="{
text: order.id,
classes: [],
max_length: 7,
}"
v-bind:visibility="{
icon: true,
dropdown: true,
}"
v-bind:dropdown_content="{
title: null,
buttons_title: null,
content: [
// Transaction
{
text: SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.single),
action: () => {
redirectDepartmentOrderPage(order.id, order.department_id);
<td class="is-narrow pos-orders-table-id-cell">
<div class="pos-orders-table-id-cell-content">
<ColorIndicator
v-bind:color_class="getOrderIndicatorColorClass(order)"
v-bind:icon_class="getOrderIndicatorIconClass(order)"
v-bind:is_narrow="true"
v-bind:label="{
text: order.id,
classes: [],
max_length: 7,
}"
v-bind:visibility="{
icon: true,
dropdown: true,
}"
v-bind:dropdown_content="{
title: null,
buttons_title: null,
content: [
// Transaction
{
text: SessionUser.functions.ucFirst(SessionUser.objects.orders.meta.labels.single),
action: () => {
redirectDepartmentOrderPage(order.id, order.department_id);
},
...(SessionUser.canAccessSuperUser() ? {} : { disabled: true }),
button_classes: ['no-underline-text', 'is-text'],
button: true,
button_text: order.id,
v_centered: true,
},
...(SessionUser.canAccessSuperUser() ? {} : { disabled: true }),
button_classes: ['no-underline-text', 'is-text'],
button: true,
button_text: order.id,
v_centered: true,
},
// Invoice collection
{
text:
SessionUser.objects.global.language.invoice +
' ' +
SessionUser.objects.global.language.collection.toLowerCase(),
action: () => {
redirectSuperUserInvoiceCollectionPage(order.invoice_collection_id);
// Invoice collection
{
text:
SessionUser.objects.global.language.invoice +
' ' +
SessionUser.objects.global.language.collection.toLowerCase(),
action: () => {
redirectSuperUserInvoiceCollectionPage(order.invoice_collection_id);
},
...(SessionUser.canAccessSuperUser() ? {} : { disabled: true }),
button_classes: ['no-underline-text', 'is-text'],
button: true,
button_text: order.invoice_collection_id,
v_centered: true,
},
...(SessionUser.canAccessSuperUser() ? {} : { disabled: true }),
button_classes: ['no-underline-text', 'is-text'],
button: true,
button_text: order.invoice_collection_id,
v_centered: true,
},
/**
/**
* // Completed status
* {
* text: SessionUser.objects.orders.columns.completed_at.label,
@@ -1392,23 +1355,24 @@ const formatCashierName = (order) => {
* },
*/
// Handheld status
...(isPendingHandheld(order)
? [
{
text: SessionUser.objects.global.language.handheld_pending_order,
action: () => {},
button_classes: ['no-underline-text', 'is-text', 'has-text-warning'],
button: true,
button_text: SessionUser.objects.global.language.confirmation_needed,
v_centered: true,
},
]
: []),
],
buttons: [],
}"
/>
// Handheld status
...(isPendingHandheld(order)
? [
{
text: SessionUser.objects.global.language.handheld_pending_order,
action: () => {},
button_classes: ['no-underline-text', 'is-text', 'has-text-warning'],
button: true,
button_text: SessionUser.objects.global.language.confirmation_needed,
v_centered: true,
},
]
: []),
],
buttons: [],
}"
/>
</div>
</td>
<td v-if="!props.isCustomerView">
<ColorIndicator
@@ -1462,8 +1426,8 @@ const formatCashierName = (order) => {
</td>
<td>{{ getDepartmentName(order.department_id) }}</td>
<!-- Vehicles -->
<td>
<p>
<td class="pos-order-vehicle-cell">
<p class="pos-order-vehicle-registration-line">
<EditableTableColumn
:componentWrapper="'div'"
:object="order"
@@ -1474,7 +1438,7 @@ const formatCashierName = (order) => {
:permissionCheckFunction="(orderobj) => canEditTransaction(order)"
/>
</p>
<p>
<p class="pos-order-vehicle-registration-line">
<EditableTableColumn
:componentWrapper="'div'"
:object="order"
@@ -1485,7 +1449,7 @@ const formatCashierName = (order) => {
:permissionCheckFunction="(bookingobj) => canEditTransaction(order)"
/>
</p>
<p v-show="order.reg_3">
<p v-show="order.reg_3" class="pos-order-vehicle-registration-line">
<EditableTableColumn
:componentWrapper="'div'"
:object="order"
@@ -1528,8 +1492,8 @@ const formatCashierName = (order) => {
<td>{{ order.created_at }}</td>
<td>{{ SessionUser.functions.currency.toLocal(order.total_net_amount) }}</td>
<!--<td>{{ order.completed_at }}</td>-->
<td class="is-narrow">
<div class="buttons pos-order-list-actions">
<td class="is-narrow pos-orders-table-actions-cell">
<SuperuserInvoiceRowActions class="pos-order-list-actions">
<button
v-if="shouldShowDraftAssignmentActions"
class="button is-small is-link is-light pos-order-list-assign-customer-button"
@@ -1581,7 +1545,7 @@ const formatCashierName = (order) => {
-->
</template>
</ActionSettingsWheelButton>
</div>
</SuperuserInvoiceRowActions>
</td>
</tr>
<tr
@@ -1627,74 +1591,17 @@ const formatCashierName = (order) => {
<div class="columns is-multiline is-mobile">
<!-- Bulk actions when invoice view allows selection -->
<div class="column is-full" v-if="props.invoiceView && props.allowSelectMultiple">
<div class="buttons">
<button
class="button is-small"
@click="invoiceSelectedCollections()"
:disabled="selectedInvoiceCollections.length === 0 || isInvoiceQueueBusy"
>
{{ SessionUser.objects.global.language.invoice }}
{{ SessionUser.objects.global.language.selected_multiple }} ({{ selectedInvoiceCollections.length }})
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.remove_customer_rule_violations") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.MERGE)"
:disabled="selectedInvoiceCollections.length < 2"
>
{{ t("invoicing_period.invoice_collection_actions.actions.merge_collections") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.SPLIT_BY_MONTH)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.split_by_month") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.RESET_HIDDEN_PRICES)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.reset_hidden_item_prices") }}
</button>
<button
class="button is-small"
@click="selectAllInvoiceCollections()"
:class="{
'is-light': !isInvoiceCollectionSelectedAll(),
'is-dark': isInvoiceCollectionSelectedAll(),
}"
>
{{
isInvoiceCollectionSelectedAll()
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
{{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
<button
class="button is-small"
@click="toggleAutoExpandAll()"
:class="{
'is-light': !isAutoExpandAll(),
'is-dark': isAutoExpandAll(),
}"
>
{{
isAutoExpandAll()
? SessionUser.objects.global.language.collapse
: SessionUser.objects.global.language.expand
}}
{{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
</div>
<InvoiceCollectionSelectionActionWheel
:selected-invoice-collection-ids="selectedInvoiceCollections"
:total-invoice-collection-count="selectableInvoiceCollectionCount"
:all-selected="isInvoiceCollectionSelectedAll()"
:all-expanded="isAutoExpandAll()"
:invoice-queue-busy="isInvoiceQueueBusy"
@invoice-selected="invoiceSelectedCollections"
@bulk-action="runInvoiceCollectionBulkAction"
@toggle-select-all="selectAllInvoiceCollections"
@toggle-expand-all="toggleAutoExpandAll"
/>
</div>
<!-- Orders as cards -->
@@ -1738,19 +1645,16 @@ const formatCashierName = (order) => {
</span>
</div>
<div class="column is-12 has-text-right mb-2">
<span class="buttons is-right mr-1">
<button
<span class="pos-order-invoice-collection-selector-wrapper mr-1">
<BCheckbox
v-if="props.invoiceView && props.allowSelectMultiple"
class="button is-light is-small"
@click="toggleInvoiceCollectionSelection(order.invoice_collection_id)"
:class="{ 'is-dark': isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)) }"
>
{{
isInvoiceCollectionSelected(parseInt(order.invoice_collection_id))
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
</button>
class="pos-order-invoice-collection-selector"
:model-value="isInvoiceCollectionSelected(order.invoice_collection_id)"
:disabled="normalizeInvoiceCollectionId(order.invoice_collection_id) === null"
:aria-label="`${SessionUser.objects.global.language.select} ${SessionUser.objects.global.language.invoice.toLowerCase()} #${order.invoice_collection_id || order.id}`"
:data-testid="`pos-order-invoice-collection-selector-${order.id}`"
@update:model-value="(checked) => setInvoiceCollectionSelection(order.invoice_collection_id, checked)"
/>
</span>
</div>
</div>
@@ -1872,74 +1776,17 @@ const formatCashierName = (order) => {
<div class="columns is-multiline is-mobile">
<!-- Bulk actions when invoice view allows selection -->
<div class="column is-full" v-if="props.invoiceView && props.allowSelectMultiple">
<div class="buttons">
<button
class="button is-small"
@click="invoiceSelectedCollections()"
:disabled="selectedInvoiceCollections.length === 0 || isInvoiceQueueBusy"
>
{{ SessionUser.objects.global.language.invoice }}
{{ SessionUser.objects.global.language.selected_multiple }} ({{ selectedInvoiceCollections.length }})
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.remove_customer_rule_violations") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.MERGE)"
:disabled="selectedInvoiceCollections.length < 2"
>
{{ t("invoicing_period.invoice_collection_actions.actions.merge_collections") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.SPLIT_BY_MONTH)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.split_by_month") }}
</button>
<button
class="button is-small"
@click="runInvoiceCollectionBulkAction(INVOICE_COLLECTION_BULK_ACTIONS.RESET_HIDDEN_PRICES)"
:disabled="selectedInvoiceCollections.length === 0"
>
{{ t("invoicing_period.invoice_collection_actions.actions.reset_hidden_item_prices") }}
</button>
<button
class="button is-small"
@click="selectAllInvoiceCollections()"
:class="{
'is-light': !isInvoiceCollectionSelectedAll(),
'is-dark': isInvoiceCollectionSelectedAll(),
}"
>
{{
isInvoiceCollectionSelectedAll()
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
{{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
<button
class="button is-small"
@click="toggleAutoExpandAll()"
:class="{
'is-light': !isAutoExpandAll(),
'is-dark': isAutoExpandAll(),
}"
>
{{
isAutoExpandAll()
? SessionUser.objects.global.language.collapse
: SessionUser.objects.global.language.expand
}}
{{ SessionUser.objects.global.language.all.toLowerCase() }}
</button>
</div>
<InvoiceCollectionSelectionActionWheel
:selected-invoice-collection-ids="selectedInvoiceCollections"
:total-invoice-collection-count="selectableInvoiceCollectionCount"
:all-selected="isInvoiceCollectionSelectedAll()"
:all-expanded="isAutoExpandAll()"
:invoice-queue-busy="isInvoiceQueueBusy"
@invoice-selected="invoiceSelectedCollections"
@bulk-action="runInvoiceCollectionBulkAction"
@toggle-select-all="selectAllInvoiceCollections"
@toggle-expand-all="toggleAutoExpandAll"
/>
</div>
<!-- Orders as cards -->
@@ -2035,19 +1882,16 @@ const formatCashierName = (order) => {
</span>
</div>
<div class="column is-12 has-text-right mb-2">
<span class="buttons is-right mr-1">
<button
<span class="pos-order-invoice-collection-selector-wrapper mr-1">
<BCheckbox
v-if="props.invoiceView && props.allowSelectMultiple"
class="button is-light is-small"
@click="toggleInvoiceCollectionSelection(order.invoice_collection_id)"
:class="{ 'is-dark': isInvoiceCollectionSelected(parseInt(order.invoice_collection_id)) }"
>
{{
isInvoiceCollectionSelected(parseInt(order.invoice_collection_id))
? SessionUser.objects.global.language.unselect
: SessionUser.objects.global.language.select
}}
</button>
class="pos-order-invoice-collection-selector"
:model-value="isInvoiceCollectionSelected(order.invoice_collection_id)"
:disabled="normalizeInvoiceCollectionId(order.invoice_collection_id) === null"
:aria-label="`${SessionUser.objects.global.language.select} ${SessionUser.objects.global.language.invoice.toLowerCase()} #${order.invoice_collection_id || order.id}`"
:data-testid="`pos-order-invoice-collection-selector-${order.id}`"
@update:model-value="(checked) => setInvoiceCollectionSelection(order.invoice_collection_id, checked)"
/>
</span>
</div>
</div>
@@ -2415,6 +2259,111 @@ const formatCashierName = (order) => {
padding-inline: 0.85rem;
}
.pos-orders-table-row > td {
vertical-align: middle;
}
.pos-orders-table-id-header,
.pos-orders-table-id-cell {
width: 5.75rem;
max-width: 5.75rem;
min-width: 5.75rem;
}
.pos-orders-table-id-cell {
text-align: center;
white-space: nowrap;
}
.pos-orders-table-id-cell-content {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.pos-orders-table-id-cell :deep(.color-indicator) {
display: block;
text-align: center;
}
.pos-orders-table-id-cell :deep(.icon-text) {
display: inline-flex;
align-items: center;
justify-content: center;
}
.pos-orders-table-actions-cell {
text-align: right;
white-space: nowrap;
}
.pos-order-invoice-collection-selector-cell {
text-align: center;
vertical-align: middle;
}
.pos-order-invoice-collection-selector-wrapper {
display: inline-flex;
align-items: center;
justify-content: center;
}
.pos-order-invoice-collection-selector {
--bulma-checkbox-size: 1.35rem;
width: 2rem;
min-width: 2rem;
height: 2rem;
display: inline-flex;
align-items: center;
justify-content: center;
margin-inline-end: 0;
vertical-align: middle;
}
.pos-order-invoice-collection-selector :deep(.check) {
margin: 0;
}
.pos-orders-table-header--compact {
white-space: nowrap;
}
.pos-orders-table-header--compact .pos-orders-table-header__label {
display: inline-block;
font-size: var(--pleno-compact-table-header-font-size);
line-height: var(--pleno-compact-table-header-line-height);
max-width: 6rem;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
}
.pos-orders-table-header--compact.pos-orders-table-header--vehicles .pos-orders-table-header__label {
max-width: 4.5rem;
}
.pos-order-vehicle-cell {
width: 4.5rem;
max-width: 4.5rem;
min-width: 4.5rem;
vertical-align: middle;
}
.pos-order-vehicle-registration-line {
max-width: 4.5rem;
margin: 0;
font-size: var(--pleno-compact-table-header-font-size);
line-height: 1.15;
}
.pos-order-vehicle-registration-line :deep(*) {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media screen and (max-width: 768px) {
.pos-orders-mobile-card {
border: 1px solid #d7dee8;
@@ -176,7 +176,7 @@ const onClickAttachToOrder = (object) => {
}"
@click="onTableHeaderClick(tableHeaders.name)"
>
<span class="is-flex-wrap-nowrap">
<span class="is-flex-wrap-nowrap pleno-table-header-content">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
<span
v-if="isColumnCurrentlyBeingSortedBy(tableHeaders.name)"
@@ -194,7 +194,7 @@ const onClickAttachToOrder = (object) => {
'has-text-centered': tableHeaders.name === 'id'
}"
>
<span class="is-flex-wrap-nowrap">
<span class="is-flex-wrap-nowrap pleno-table-header-content">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
</span>
</th>
@@ -453,7 +453,7 @@ const confirmCloseModal = () => {
}"
@click="onTableHeaderClick(tableHeaders.name)"
>
<span class="is-flex-wrap-nowrap">
<span class="is-flex-wrap-nowrap pleno-table-header-content">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
<span
v-if="isColumnCurrentlyBeingSortedBy(tableHeaders.name)"
@@ -471,7 +471,7 @@ const confirmCloseModal = () => {
'has-text-centered': tableHeaders.name === 'id'
}"
>
<span class="is-flex-wrap-nowrap">
<span class="is-flex-wrap-nowrap pleno-table-header-content">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
</span>
</th>
@@ -512,7 +512,7 @@ const confirmCloseModal = () => {
</button>
</td>
</template>
<td>
<td class="has-text-centered">
<ColorIndicator
v-bind:color_class="getOrderInvoiceStatusBarColor(order)"
v-bind:is_narrow="true"
@@ -651,7 +651,7 @@ const filteredObjects = computed(() => {
}"
@click="onTableHeaderClick(tableHeaders.name)"
>
<span class="is-flex-wrap-nowrap">
<span class="is-flex-wrap-nowrap pleno-table-header-content">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
<span
v-if="isColumnCurrentlyBeingSortedBy(tableHeaders.name)"
@@ -669,7 +669,7 @@ const filteredObjects = computed(() => {
'has-text-centered': tableHeaders.name === 'id'
}"
>
<span class="is-flex-wrap-nowrap">
<span class="is-flex-wrap-nowrap pleno-table-header-content">
<span class="has-text-weight-bold">{{ tableHeaders.title }}</span>
</span>
</th>
@@ -7,6 +7,7 @@ const { t } = useI18n();
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import SuperuserInvoiceRowActions from "@/components/displays/superuser/tables/SuperuserInvoiceRowActions.vue";
const props = defineProps({
objects: {
@@ -65,8 +66,8 @@ const parseClosedAt = (value) => {
<td>{{ parseClosedAt(object.closed_at) }}</td>
<td>{{ parseDate(object.created_at) }}</td>
<td>{{ parseDate(object.updated_at) }}</td>
<td>
<div class="buttons is-float-right">
<td class="is-narrow">
<SuperuserInvoiceRowActions :include-buffer="true">
<ActionSettingsWheelButton>
<template #actions>
<ActionSettingsWheelItem
@@ -76,7 +77,7 @@ const parseClosedAt = (value) => {
/>
</template>
</ActionSettingsWheelButton>
</div>
</SuperuserInvoiceRowActions>
</td>
</tr>
<tr v-if="objects.length === 0">
@@ -0,0 +1,49 @@
<script setup>
defineProps({
includeBuffer: {
type: Boolean,
default: false,
},
});
</script>
<template>
<div class="buttons superuser-invoice-row-actions">
<button
v-if="includeBuffer"
type="button"
class="button is-small superuser-invoice-row-actions__buffer"
aria-hidden="true"
tabindex="-1"
disabled
data-testid="superuser-invoice-action-buffer"
>
<span class="icon">
<i class="fas fa-paperclip"></i>
</span>
</button>
<slot />
</div>
</template>
<style scoped>
.superuser-invoice-row-actions {
flex-wrap: nowrap;
justify-content: flex-end;
gap: 0.5rem;
margin-bottom: 0;
}
.superuser-invoice-row-actions > :deep(*) {
flex-shrink: 0;
}
.superuser-invoice-row-actions__buffer {
width: 2rem;
min-width: 2rem;
height: 2rem;
padding: 0;
visibility: hidden;
pointer-events: none;
}
</style>
@@ -1,11 +1,10 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { BCheckbox } from "buefy";
const { t } = useI18n();
import EditableTableColumn from "@/components/displays/buttons/EditableTableColumn.vue";
import {getCustomerName} from "@/components/shop/POSDepartmentProcess.vue";
import { ref } from 'vue';
const props = defineProps({
objects: {
type: Array,
@@ -35,6 +34,7 @@ const { loadList } = usePaginatedListInstance();
import { SessionUser } from "@/components/session/token/SessionUser.vue";
import ActionSettingsWheelButton from "@/components/displays/buttons/ActionSettingsWheelButton.vue";
import ActionSettingsWheelItem from "@/components/displays/buttons/ActionSettingsWheelItem.vue";
import SuperuserInvoiceRowActions from "@/components/displays/superuser/tables/SuperuserInvoiceRowActions.vue";
const redirect = (path) => {
// Open the url in a new tab
@@ -59,6 +59,12 @@ const onSelect = (id) => {
props.onSelected(id);
}
const onSelectorChecked = (id, checked) => {
if (checked) {
onSelect(id);
}
}
</script>
<template>
@@ -124,22 +130,28 @@ const onSelect = (id) => {
<!-- Total net amount -->
<td v-if="props.showTotal">{{ SessionUser.functions.currency.toLocal(object.total_net_amount) }}</td>
<!-- Actions -->
<td v-if="!props.isSelector">
<ActionSettingsWheelButton>
<template #actions>
<ActionSettingsWheelItem
icon="fas fa-eye"
@click="redirect('/superuser/invoices/' + object.id)"
:label="t('common.show')"
/>
</template>
</ActionSettingsWheelButton>
<td v-if="!props.isSelector" class="is-narrow">
<SuperuserInvoiceRowActions :include-buffer="true">
<ActionSettingsWheelButton>
<template #actions>
<ActionSettingsWheelItem
icon="fas fa-eye"
@click="redirect('/superuser/invoices/' + object.id)"
:label="t('common.show')"
/>
</template>
</ActionSettingsWheelButton>
</SuperuserInvoiceRowActions>
</td>
<!-- Selector -->
<td v-if="props.isSelector">
<button class="button is-small is-dark" @click="onSelect(object.id)">
{{ $t('common.select') }}
</button>
<td v-if="props.isSelector" class="is-narrow">
<BCheckbox
:model-value="false"
size="is-small"
:aria-label="`${$t('common.select')} #${object.id}`"
:data-testid="`collected-invoice-selector-${object.id}`"
@update:model-value="(checked) => onSelectorChecked(object.id, checked)"
/>
</td>
</tr>
</template>
@@ -159,4 +171,4 @@ const onSelect = (id) => {
<style scoped>
</style>
</style>
+9
View File
@@ -4152,6 +4152,15 @@
"merge_requires_multiple_text": "Vælg mindst to fakturasamlinger for at sammenlægge dem.",
"merge_requires_multiple_title": "Vælg flere fakturasamlinger",
"merge_target_title": "Vælg målfakturasamling",
"menu": {
"collapse_all": "Klap alle sammen",
"expand_all": "Udvid alle",
"label": "Fakturasamling handlinger ({count})",
"modification_section": "Rediger valgte",
"select_all": "Vælg alle",
"selection_section": "Valg",
"unselect_all": "Fravælg alle"
},
"preview": {
"affected_examples": "Eksempler på ændringer",
"blocked_title": "Handlingen er blokeret",
+9
View File
@@ -4263,6 +4263,15 @@
"merge_requires_multiple_text": "Wählen Sie mindestens zwei Rechnungssammlungen aus, um sie zusammenzuführen.",
"merge_requires_multiple_title": "Mehrere Rechnungssammlungen auswählen",
"merge_target_title": "Ziel-Rechnungssammlung auswählen",
"menu": {
"collapse_all": "Alle einklappen",
"expand_all": "Alle erweitern",
"label": "Rechnungssammlungsaktionen ({count})",
"modification_section": "Ausgewählte ändern",
"select_all": "Alle auswählen",
"selection_section": "Auswahl",
"unselect_all": "Alle abwählen"
},
"preview": {
"affected_examples": "Beispieländerungen",
"blocked_title": "Aktion ist blockiert",
+9
View File
@@ -3984,6 +3984,15 @@
"merge_requires_multiple_text": "Select at least two invoice collections to merge them.",
"merge_requires_multiple_title": "Select multiple invoice collections",
"merge_target_title": "Select target invoice collection",
"menu": {
"collapse_all": "Collapse all",
"expand_all": "Expand all",
"label": "Invoice collection actions ({count})",
"modification_section": "Modify selected",
"select_all": "Select all",
"selection_section": "Selection",
"unselect_all": "Unselect all"
},
"preview": {
"affected_examples": "Example changes",
"blocked_title": "Action is blocked",
+9
View File
@@ -3237,6 +3237,15 @@
"merge_requires_multiple_text": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.merge_requires_multiple_text'}",
"merge_requires_multiple_title": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.merge_requires_multiple_title'}",
"merge_target_title": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.merge_target_title'}",
"menu": {
"collapse_all": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.collapse_all'}",
"expand_all": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.expand_all'}",
"label": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.label'}",
"modification_section": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.modification_section'}",
"select_all": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.select_all'}",
"selection_section": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.selection_section'}",
"unselect_all": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.menu.unselect_all'}"
},
"preview": {
"affected_examples": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.affected_examples'}",
"blocked_title": "@:{'templates.generated.compat.invoicing_period.invoice_collection_actions.preview.blocked_title'}",
+9
View File
@@ -4266,6 +4266,15 @@
"merge_requires_multiple_text": "Velg minst to fakturasamlinger for å slå dem sammen.",
"merge_requires_multiple_title": "Velg flere fakturasamlinger",
"merge_target_title": "Velg målfakturasamling",
"menu": {
"collapse_all": "Slå sammen alle",
"expand_all": "Utvid alle",
"label": "Fakturasamling handlinger ({count})",
"modification_section": "Endre valgte",
"select_all": "Velg alle",
"selection_section": "Valg",
"unselect_all": "Fjern valg av alle"
},
"preview": {
"affected_examples": "Eksempler på endringer",
"blocked_title": "Handlingen er blokkert",
+9
View File
@@ -4316,6 +4316,15 @@
"merge_requires_multiple_text": "Välj minst två fakturasamlingar för att slå ihop dem.",
"merge_requires_multiple_title": "Välj flera fakturasamlingar",
"merge_target_title": "Välj målfakturasamling",
"menu": {
"collapse_all": "Fäll ihop alla",
"expand_all": "Expandera alla",
"label": "Fakturasamlingsåtgärder ({count})",
"modification_section": "Ändra valda",
"select_all": "Välj alla",
"selection_section": "Val",
"unselect_all": "Avmarkera alla"
},
"preview": {
"affected_examples": "Exempel på ändringar",
"blocked_title": "Åtgärden är blockerad",
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Vælg mindst to fakturasamlinger for at sammenlægge dem.",
"merge_requires_multiple_title": "Vælg flere fakturasamlinger",
"merge_target_title": "Vælg målfakturasamling",
"menu": {
"collapse_all": "Klap alle sammen",
"expand_all": "Udvid alle",
"label": "Fakturasamling handlinger ({count})",
"modification_section": "Rediger valgte",
"select_all": "Vælg alle",
"selection_section": "Valg",
"unselect_all": "Fravælg alle"
},
"preview": {
"affected_examples": "Eksempler på ændringer",
"blocked_title": "Handlingen er blokeret",
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Wählen Sie mindestens zwei Rechnungssammlungen aus, um sie zusammenzuführen.",
"merge_requires_multiple_title": "Mehrere Rechnungssammlungen auswählen",
"merge_target_title": "Ziel-Rechnungssammlung auswählen",
"menu": {
"collapse_all": "Alle einklappen",
"expand_all": "Alle erweitern",
"label": "Rechnungssammlungsaktionen ({count})",
"modification_section": "Ausgewählte ändern",
"select_all": "Alle auswählen",
"selection_section": "Auswahl",
"unselect_all": "Alle abwählen"
},
"preview": {
"affected_examples": "Beispieländerungen",
"blocked_title": "Aktion ist blockiert",
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Select at least two invoice collections to merge them.",
"merge_requires_multiple_title": "Select multiple invoice collections",
"merge_target_title": "Select target invoice collection",
"menu": {
"collapse_all": "Collapse all",
"expand_all": "Expand all",
"label": "Invoice collection actions ({count})",
"modification_section": "Modify selected",
"select_all": "Select all",
"selection_section": "Selection",
"unselect_all": "Unselect all"
},
"preview": {
"affected_examples": "Example changes",
"blocked_title": "Action is blocked",
@@ -12,6 +12,15 @@
"merge_requires_multiple_text": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.merge_requires_multiple_text'}",
"merge_requires_multiple_title": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.merge_requires_multiple_title'}",
"merge_target_title": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.merge_target_title'}",
"menu": {
"collapse_all": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.collapse_all'}",
"expand_all": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.expand_all'}",
"label": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.label'}",
"modification_section": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.modification_section'}",
"select_all": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.select_all'}",
"selection_section": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.selection_section'}",
"unselect_all": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.menu.unselect_all'}"
},
"preview": {
"affected_examples": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.affected_examples'}",
"blocked_title": "@:{'phrases.compat.invoicing_period.invoice_collection_actions.preview.blocked_title'}",
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Velg minst to fakturasamlinger for å slå dem sammen.",
"merge_requires_multiple_title": "Velg flere fakturasamlinger",
"merge_target_title": "Velg målfakturasamling",
"menu": {
"collapse_all": "Slå sammen alle",
"expand_all": "Utvid alle",
"label": "Fakturasamling handlinger ({count})",
"modification_section": "Endre valgte",
"select_all": "Velg alle",
"selection_section": "Valg",
"unselect_all": "Fjern valg av alle"
},
"preview": {
"affected_examples": "Eksempler på endringer",
"blocked_title": "Handlingen er blokkert",
@@ -13,6 +13,15 @@
"merge_requires_multiple_text": "Välj minst två fakturasamlingar för att slå ihop dem.",
"merge_requires_multiple_title": "Välj flera fakturasamlingar",
"merge_target_title": "Välj målfakturasamling",
"menu": {
"collapse_all": "Fäll ihop alla",
"expand_all": "Expandera alla",
"label": "Fakturasamlingsåtgärder ({count})",
"modification_section": "Ändra valda",
"select_all": "Välj alla",
"selection_section": "Val",
"unselect_all": "Avmarkera alla"
},
"preview": {
"affected_examples": "Exempel på ändringar",
"blocked_title": "Åtgärden är blockerad",
+1
View File
@@ -23,6 +23,7 @@ import {
installReleaseErrorInstrumentation,
} from '@/services/releaseTimeline.js';
import { RELEASE_RUNTIME_GLOBAL_KEY } from '@/services/releaseBootstrap.js';
import '@/services/tableHeaderPreferences.js';
import { IS_DEV } from './config';
const VITE_BUILD_DATE = import.meta.env.VITE_BUILD_DATE || '';
+52
View File
@@ -0,0 +1,52 @@
import { ref } from "vue";
export const LARGE_TABLE_HEADERS_STORAGE_KEY = "pleno.useLargeTableHeaders";
export const LARGE_TABLE_HEADERS_BODY_CLASS = "pleno-large-table-headers";
const applyLargeTableHeadersClass = (value) => {
if (typeof document === "undefined" || !document.body) {
return;
}
document.body.classList.toggle(LARGE_TABLE_HEADERS_BODY_CLASS, Boolean(value));
};
const readStoredBoolean = () => {
if (typeof window === "undefined" || !window.localStorage) {
return false;
}
try {
return window.localStorage.getItem(LARGE_TABLE_HEADERS_STORAGE_KEY) === "true";
} catch {
return false;
}
};
export const useLargeTableHeaders = ref(readStoredBoolean());
applyLargeTableHeadersClass(useLargeTableHeaders.value);
export const setUseLargeTableHeaders = (value) => {
const nextValue = Boolean(value);
useLargeTableHeaders.value = nextValue;
applyLargeTableHeadersClass(nextValue);
if (typeof window === "undefined" || !window.localStorage) {
return;
}
try {
window.localStorage.setItem(LARGE_TABLE_HEADERS_STORAGE_KEY, nextValue ? "true" : "false");
} catch {
// Storage is optional; keep the in-memory preference for this session.
}
};
if (typeof window !== "undefined") {
window.addEventListener("storage", (event) => {
if (event.key === LARGE_TABLE_HEADERS_STORAGE_KEY) {
useLargeTableHeaders.value = event.newValue === "true";
applyLargeTableHeadersClass(useLargeTableHeaders.value);
}
});
}
@@ -15,6 +15,7 @@ import ShowErrorField from "@/components/global/ShowErrorField.vue";
import SubuserGrantSelector from "@/components/session/subuser/SubuserGrantSelector.vue";
import { parseError, removeError, addError } from "@/components/request/HandleGlobalError.vue";
import ConfigurationSwitch from "@/components/displays/superuser/configuration/ConfigurationSwitch.vue";
import { setUseLargeTableHeaders, useLargeTableHeaders } from "@/services/tableHeaderPreferences.js";
const { t } = useI18n();
const isLoading = ref(false);
@@ -792,6 +793,22 @@ const onClickSaveWashCertificateEmail = async () => {
{{ $t("user_dashboard.profile.driver.customer_switch_help") }}
</p>
</ConfigurationCategory>
<!-- Display preferences -->
<ConfigurationCategory
class="mt-2"
title="Visning"
description="Personlige visningsindstillinger for tabeller."
icon="fas fa-table"
data-testid="user-profile-display-preferences-card"
>
<ConfigurationSwitch
data-testid="user-profile-large-table-headers-switch"
title="Store tabeloverskrifter"
description="Brug de større tabeloverskrifter i stedet for den kompakte standardvisning."
:value="useLargeTableHeaders"
:on-switch="setUseLargeTableHeaders"
/>
</ConfigurationCategory>
<!-- Notifications - email notifications -->
<ConfigurationCategory
v-if="!SessionUser.isSubuser.value"
+42
View File
@@ -2613,6 +2613,48 @@ test.describe("Admin POS Orders - desktop action menu layout", () => {
await primeOperatorSession(page, "pos-orders-action-menu-layout-token", SUPERUSER_POS_PERMISSIONS);
});
test("keeps desktop order list row cells aligned", async ({ page }) => {
await page.goto("/admin/12/modules/pos/orders");
await expect(page.locator("table")).toBeVisible();
const row = getVisibleTestId(page, "pos-order-list-settings-54518").locator("xpath=ancestor::tr").first();
await expect(row).toBeVisible();
const rowMetrics = await row.evaluate((rowElement) => {
const cells = Array.from(rowElement.children).filter((child) => child.tagName.toLowerCase() === "td");
const idCell = rowElement.querySelector(".pos-orders-table-id-cell");
const idIndicator = idCell?.querySelector(".pos-orders-table-id-cell-content");
const cellSummaries = cells.map((cell) => {
const rect = cell.getBoundingClientRect();
return {
className: cell.className,
verticalAlign: window.getComputedStyle(cell).verticalAlign,
width: rect.width,
};
});
const idCellRect = idCell?.getBoundingClientRect();
const idIndicatorRect = idIndicator?.getBoundingClientRect();
return {
cellSummaries,
idCenterDelta:
idCellRect && idIndicatorRect
? Math.abs(idIndicatorRect.left + idIndicatorRect.width / 2 - (idCellRect.left + idCellRect.width / 2))
: null,
idCellWidth: idCellRect?.width ?? null,
};
});
expect(rowMetrics.cellSummaries.length).toBeGreaterThan(5);
expect(rowMetrics.cellSummaries.every((cell) => cell.verticalAlign === "middle")).toBe(true);
expect(rowMetrics.idCenterDelta).not.toBeNull();
expect(rowMetrics.idCenterDelta ?? 999).toBeLessThanOrEqual(2);
expect(rowMetrics.idCellWidth ?? 999).toBeLessThanOrEqual(100);
expect(
rowMetrics.cellSummaries.some((cell) => String(cell.className).includes("pos-orders-table-actions-cell"))
).toBe(true);
});
test("keeps tall menus within the usable viewport and starts from the first action", async ({ page }) => {
await page.goto("/admin/12/modules/pos/orders");
await expect(page.locator("table")).toBeVisible();
+3 -3
View File
@@ -166,11 +166,11 @@ test.describe("POS order actions", () => {
"Expected invoice collection picker modal to open after clicking the action."
).toBeVisible();
const selectButtons = pickerModal.getByRole("button", { name: /Vælg|Select/i });
const selectableCount = await selectButtons.count();
const selectCheckboxes = pickerModal.locator('[data-testid^="collected-invoice-selector-"] input[type="checkbox"]');
const selectableCount = await selectCheckboxes.count();
test.skip(selectableCount === 0, "No invoice collections available for selection in test data.");
await selectButtons.first().click();
await selectCheckboxes.first().check();
await expect(page).toHaveURL(beforeUrl);
await expect(page.locator(".swal2-container, .swal2-popup")).toContainText(
@@ -0,0 +1,186 @@
// @vitest-environment jsdom
import { mount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
import CollectedOrderInvoicesTable from "@/components/displays/superuser/tables/collectedOrderInvoicesTable.vue";
import CollectedOrderInvoicesDefaultTable from "@/components/displays/superuser/tables/CollectedOrderInvoicesDefaultTable.vue";
vi.mock("@/components/pagination/paginatedList.vue", () => ({
usePaginatedListInstance: () => ({
loadList: vi.fn(),
}),
}));
vi.mock("@/components/session/token/SessionUser.vue", () => ({
SessionUser: {
objects: {
collectedOrderInvoices: {
columns: {
id: { label: "ID" },
customer_number: { label: "Customer number" },
processor: { label: "Processor" },
created_at: { label: "Created" },
name: { label: "Name" },
notes: { label: "Notes" },
external_id: { label: "External ID" },
po_number: { label: "PO" },
closed_at: { label: "Closed" },
updated_at: { label: "Updated" },
},
meta: {
labels: {
multiple: "invoice collections",
},
},
},
global: {
language: {
customer_name: "Customer",
},
},
orders: {
meta: {
title: "Orders",
},
},
},
functions: {
currency: {
toLocal: (value) => String(value ?? ""),
},
},
},
}));
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key) => key,
}),
createI18n: () => ({
global: {
t: (key) => key,
locale: {
value: "en",
},
},
}),
}));
const EditableTableColumnStub = {
props: ["object", "column", "parseFunction"],
template: "<td>{{ parseFunction ? parseFunction(object[column]) : object[column] }}</td>",
};
const ActionSettingsWheelButtonStub = {
template: '<div data-testid="action-settings-wheel-stub"><slot name="actions" /></div>',
};
const ActionSettingsWheelItemStub = {
template: '<button type="button" data-testid="action-settings-wheel-item-stub"></button>',
};
const BCheckboxStub = {
props: ["modelValue"],
emits: ["update:modelValue"],
template: `
<label :data-testid="$attrs['data-testid']">
<input
type="checkbox"
:aria-label="$attrs['aria-label']"
:checked="modelValue"
@change="$emit('update:modelValue', $event.target.checked)"
/>
</label>
`,
};
const mountSelectorTable = (props = {}) =>
mount(CollectedOrderInvoicesTable, {
props: {
objects: [
{
id: 42,
objects: 1,
customer_name: "Acme",
customer_number: 1001,
processor: 1,
orders: 2,
closed_at: null,
created_at: "2026-07-01",
total_net_amount: 123,
},
],
showEmpty: true,
...props,
},
global: {
mocks: {
$t: (key) => key,
},
stubs: {
EditableTableColumn: EditableTableColumnStub,
ActionSettingsWheelButton: ActionSettingsWheelButtonStub,
ActionSettingsWheelItem: ActionSettingsWheelItemStub,
BCheckbox: BCheckboxStub,
},
},
});
describe("CollectedOrderInvoicesTable", () => {
it("uses a Buefy checkbox for selector mode", async () => {
const onSelected = vi.fn();
const wrapper = mountSelectorTable({
isSelector: true,
onSelected,
});
expect(wrapper.find("button.button.is-small.is-dark").exists()).toBe(false);
await wrapper.get('[data-testid="collected-invoice-selector-42"] input').setValue(true);
expect(onSelected).toHaveBeenCalledWith(42);
expect(wrapper.emitted("update:selected")?.[0]).toEqual([42]);
});
it("keeps a hidden attachment-width buffer before collected invoice row actions", () => {
const wrapper = mountSelectorTable();
expect(wrapper.get('[data-testid="superuser-invoice-action-buffer"]').exists()).toBe(true);
expect(wrapper.get('[data-testid="action-settings-wheel-stub"]').exists()).toBe(true);
});
});
describe("CollectedOrderInvoicesDefaultTable", () => {
it("keeps a hidden attachment-width buffer before default invoice row actions", () => {
const wrapper = mount(CollectedOrderInvoicesDefaultTable, {
props: {
objects: [
{
id: 43,
customer_number: 1002,
name: "Acme July",
notes: null,
processor: 1,
external_id: null,
po_number: null,
closed_at: null,
created_at: "2026-07-01",
updated_at: "2026-07-02",
},
],
},
global: {
mocks: {
$t: (key) => key,
},
stubs: {
ActionSettingsWheelButton: ActionSettingsWheelButtonStub,
ActionSettingsWheelItem: ActionSettingsWheelItemStub,
},
},
});
expect(wrapper.get('[data-testid="superuser-invoice-action-buffer"]').exists()).toBe(true);
expect(wrapper.get('[data-testid="action-settings-wheel-stub"]').exists()).toBe(true);
});
});
@@ -0,0 +1,187 @@
// @vitest-environment jsdom
import { defineComponent } from "vue";
import { mount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
import InvoiceCollectionSelectionActionWheel from "@/components/displays/department/pos/orders/InvoiceCollectionSelectionActionWheel.vue";
import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key, params = {}) => (params.count === undefined ? key : `${key}:${params.count}`),
}),
createI18n: () => ({
global: {
t: (key) => key,
},
install: () => {},
}),
}));
vi.mock("@/components/displays/buttons/ActionSettingsWheelButton.vue", () => ({
default: {
template: '<div data-testid="settings-wheel-stub"><slot name="actions"></slot></div>',
},
}));
vi.mock("@/components/displays/buttons/ActionSettingsWheelItemLabel.vue", () => ({
default: {
props: ["label"],
template: '<div class="action-settings-wheel-label-stub">{{ label }}</div>',
},
}));
vi.mock("@/components/displays/buttons/ActionSettingsWheelItem.vue", () => ({
default: {
props: {
clickAction: {
type: Function,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
testId: {
type: String,
default: "",
},
},
template: `
<button
type="button"
class="action-settings-wheel-item-stub"
:data-testid="testId || undefined"
:disabled="disabled"
@click="clickAction && clickAction()"
>
{{ label }}
</button>
`,
},
}));
const ActionSettingsWheelButtonStub = {
template: '<div data-testid="settings-wheel-stub"><slot name="actions"></slot></div>',
};
const ActionSettingsWheelItemLabelStub = {
props: ["label"],
template: '<div class="action-settings-wheel-label-stub">{{ label }}</div>',
};
const ActionSettingsWheelItemStub = defineComponent({
name: "ActionSettingsWheelItem",
props: {
clickAction: {
type: Function,
default: null,
},
disabled: {
type: Boolean,
default: false,
},
label: {
type: String,
default: "",
},
testId: {
type: String,
default: "",
},
},
template: `
<button
type="button"
class="action-settings-wheel-item-stub"
:data-testid="testId || undefined"
:disabled="disabled"
@click="clickAction && clickAction()"
>
{{ label }}
</button>
`,
});
const mountWheel = (props = {}) =>
mount(InvoiceCollectionSelectionActionWheel, {
props: {
selectedInvoiceCollectionIds: [],
totalInvoiceCollectionCount: 2,
allSelected: false,
allExpanded: false,
invoiceQueueBusy: false,
...props,
},
global: {
stubs: {
ActionSettingsWheelButton: ActionSettingsWheelButtonStub,
ActionSettingsWheelItem: ActionSettingsWheelItemStub,
ActionSettingsWheelItemLabel: ActionSettingsWheelItemLabelStub,
},
},
});
describe("InvoiceCollectionSelectionActionWheel", () => {
it("does not render without selectable invoice collections", () => {
const wrapper = mountWheel({ totalInvoiceCollectionCount: 0 });
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel"]').exists()).toBe(false);
});
it("shows selection controls without modification actions when nothing is selected", async () => {
const wrapper = mountWheel();
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-select-all"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-expand-all"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-invoice-selected"]').exists()).toBe(
false
);
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-merge"]').exists()).toBe(false);
await wrapper.get('[data-testid="invoice-collection-selection-action-wheel-select-all"]').trigger("click");
await wrapper.get('[data-testid="invoice-collection-selection-action-wheel-expand-all"]').trigger("click");
expect(wrapper.emitted("toggleSelectAll")).toHaveLength(1);
expect(wrapper.emitted("toggleExpandAll")).toHaveLength(1);
});
it("shows single-selection modification actions and hides merge", async () => {
const wrapper = mountWheel({ selectedInvoiceCollectionIds: [3001] });
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-invoice-selected"]').exists()).toBe(
true
);
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-clean-rules"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-split-by-month"]').exists()).toBe(
true
);
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-reset-hidden-prices"]').exists()).toBe(
true
);
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-merge"]').exists()).toBe(false);
await wrapper.get('[data-testid="invoice-collection-selection-action-wheel-clean-rules"]').trigger("click");
expect(wrapper.emitted("bulkAction")).toEqual([[INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES]]);
});
it("shows merge for multiple selections and disables invoicing while queue is busy", async () => {
const wrapper = mountWheel({
selectedInvoiceCollectionIds: [3001, 3002],
invoiceQueueBusy: true,
});
expect(
wrapper.get('[data-testid="invoice-collection-selection-action-wheel-invoice-selected"]').attributes("disabled")
).toBeDefined();
expect(wrapper.find('[data-testid="invoice-collection-selection-action-wheel-merge"]').exists()).toBe(true);
await wrapper.get('[data-testid="invoice-collection-selection-action-wheel-merge"]').trigger("click");
expect(wrapper.emitted("bulkAction")).toEqual([[INVOICE_COLLECTION_BULK_ACTIONS.MERGE]]);
});
});
+243 -1
View File
@@ -1,9 +1,13 @@
// @vitest-environment jsdom
import { defineComponent, h, ref } from "vue";
import { defineComponent, h, nextTick, ref } from "vue";
import { flushPromises, mount } from "@vue/test-utils";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { INVOICE_COLLECTION_BULK_ACTIONS } from "@/components/displays/department/pos/orders/invoiceCollectionBulkActions.js";
import { setUseLargeTableHeaders } from "@/services/tableHeaderPreferences.js";
const getSingleMock = vi.hoisted(() => vi.fn());
const bulkActionPreviewMock = vi.hoisted(() => vi.fn());
const bulkActionApplyMock = vi.hoisted(() => vi.fn());
const sessionState = vi.hoisted(() => ({
canAccessSuperUser: false,
}));
@@ -30,6 +34,9 @@ const invoiceQueueState = vi.hoisted(() => {
vi.mock("vue-i18n", () => ({
useI18n: () => ({
t: (key) => key,
locale: {
value: "en",
},
}),
createI18n: () => ({
global: {
@@ -146,6 +153,10 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
get: {
single: getSingleMock,
},
functions: {
bulk_action_preview: bulkActionPreviewMock,
bulk_action_apply: bulkActionApplyMock,
},
},
global: {
language: {
@@ -178,6 +189,7 @@ vi.mock("@/components/session/token/SessionUser.vue", () => ({
date: {
timeAgo: () => "just now",
},
parseErrorMessage: (error) => error?.message || String(error),
},
},
}));
@@ -261,6 +273,74 @@ const ActionSettingsWheelButtonStub = {
template: '<div class="action-settings-wheel-button-stub"><slot name="actions"></slot></div>',
};
const InvoiceCollectionSelectionActionWheelStub = defineComponent({
name: "InvoiceCollectionSelectionActionWheel",
props: {
selectedInvoiceCollectionIds: {
type: Array,
default: () => [],
},
totalInvoiceCollectionCount: {
type: Number,
default: 0,
},
allSelected: {
type: Boolean,
default: false,
},
allExpanded: {
type: Boolean,
default: false,
},
invoiceQueueBusy: {
type: Boolean,
default: false,
},
},
emits: ["invoiceSelected", "bulkAction", "toggleSelectAll", "toggleExpandAll"],
template: `
<div
class="invoice-collection-selection-action-wheel-stub"
:data-selected-count="selectedInvoiceCollectionIds.length"
:data-total-count="totalInvoiceCollectionCount"
:data-all-selected="String(allSelected)"
:data-all-expanded="String(allExpanded)"
:data-queue-busy="String(invoiceQueueBusy)"
>
<button data-testid="selection-wheel-invoice-selected" @click="$emit('invoiceSelected')"></button>
<button data-testid="selection-wheel-clean-rules" @click="$emit('bulkAction', '${INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES}')"></button>
<button data-testid="selection-wheel-select-all" @click="$emit('toggleSelectAll')"></button>
<button data-testid="selection-wheel-expand-all" @click="$emit('toggleExpandAll')"></button>
</div>
`,
});
const BCheckboxStub = defineComponent({
name: "BCheckbox",
props: {
modelValue: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue"],
template: `
<label :class="$attrs.class" :data-testid="$attrs['data-testid']">
<input
type="checkbox"
:aria-label="$attrs['aria-label']"
:checked="modelValue"
:disabled="disabled"
@change="$emit('update:modelValue', $event.target.checked)"
/>
</label>
`,
});
const ActionSettingsWheelItemStub = {
template: '<div class="action-settings-wheel-item-stub"></div>',
};
@@ -339,6 +419,8 @@ const mountOrdersTable = (props = {}) => {
EditableTableColumn: EditableTableColumnStub,
ColorIndicator: ColorIndicatorStub,
ActionSettingsWheelButton: ActionSettingsWheelButtonStub,
InvoiceCollectionSelectionActionWheel: InvoiceCollectionSelectionActionWheelStub,
BCheckbox: BCheckboxStub,
ActionSettingsWheelItem: ActionSettingsWheelItemStub,
OrderContentTable: OrderContentTableStub,
PosDepartmentStepMobileAttachment: PosDepartmentStepMobileAttachmentStub,
@@ -371,10 +453,26 @@ describe("OrdersTable", () => {
invoiceQueueState.success.value = [];
invoiceQueueState.queued.value = [];
invoiceQueueState.log.value = [];
setUseLargeTableHeaders(false);
getSingleMock.mockReset();
bulkActionPreviewMock.mockReset();
bulkActionPreviewMock.mockResolvedValue({
data: {
data: {
preview_id: "preview-1",
confirmation_phrase: "Confirm",
summary: {
changed_count: 0,
},
blockers: [],
},
},
});
bulkActionApplyMock.mockReset();
});
afterEach(() => {
setUseLargeTableHeaders(false);
vi.restoreAllMocks();
});
@@ -612,4 +710,148 @@ describe("OrdersTable", () => {
expect(getIndicatorForOrder(56631).attributes("data-color-class")).toBe("has-text-warning");
expect(getIndicatorForOrder(56632).attributes("data-icon")).toBe("fas fa-circle");
});
it("routes invoice collection selection controls through the action wheel", async () => {
const wrapper = mountOrdersTable({
invoiceView: true,
allowSelectMultiple: true,
autoExpandAll: false,
orders: [
createOrder({
id: 56633,
invoice_collection_id: 3001,
invoice_collection: { id: 3001, closed_at: null, booked_invoice_id: null },
}),
createOrder({
id: 56634,
invoice_collection_id: 3001,
invoice_collection: { id: 3001, closed_at: null, booked_invoice_id: null },
}),
createOrder({
id: 56635,
invoice_collection_id: 3002,
invoice_collection: { id: 3002, closed_at: null, booked_invoice_id: null },
}),
],
});
await flushPromises();
const wheel = () => wrapper.get(".invoice-collection-selection-action-wheel-stub");
const firstCollectionSelector = () =>
wrapper.get('[data-testid="pos-order-invoice-collection-selector-56633"] input');
const secondSameCollectionSelector = () =>
wrapper.get('[data-testid="pos-order-invoice-collection-selector-56634"] input');
expect(wheel().attributes("data-total-count")).toBe("2");
expect(wheel().attributes("data-selected-count")).toBe("0");
expect(wheel().attributes("data-all-selected")).toBe("false");
expect(wheel().attributes("data-all-expanded")).toBe("false");
expect(wrapper.findAll("button").some((button) => /select|unselect|vælg|fravælg/i.test(button.text()))).toBe(false);
expect(wrapper.get('[data-testid="pos-order-invoice-collection-selector-56633"]').classes()).toContain(
"pos-order-invoice-collection-selector"
);
await firstCollectionSelector().setValue(true);
await flushPromises();
expect(wheel().attributes("data-selected-count")).toBe("1");
expect(firstCollectionSelector().element.checked).toBe(true);
expect(secondSameCollectionSelector().element.checked).toBe(true);
await secondSameCollectionSelector().setValue(false);
await flushPromises();
expect(wheel().attributes("data-selected-count")).toBe("0");
expect(firstCollectionSelector().element.checked).toBe(false);
await wrapper.get('[data-testid="selection-wheel-select-all"]').trigger("click");
await flushPromises();
expect(wheel().attributes("data-selected-count")).toBe("2");
expect(wheel().attributes("data-all-selected")).toBe("true");
await wrapper.get('[data-testid="selection-wheel-expand-all"]').trigger("click");
await flushPromises();
expect(wheel().attributes("data-all-expanded")).toBe("true");
await wrapper.get('[data-testid="selection-wheel-clean-rules"]').trigger("click");
await flushPromises();
expect(bulkActionPreviewMock).toHaveBeenCalledWith(
INVOICE_COLLECTION_BULK_ACTIONS.CLEAN_CUSTOMER_RULES,
[3001, 3002],
{},
"en"
);
});
it("uses compact table headers by default and restores large headers from the user setting", async () => {
const wrapper = mountOrdersTable({
invoiceView: true,
orders: [createOrder()],
});
await flushPromises();
expect(wrapper.find("th.pos-orders-table-header--compact").exists()).toBe(true);
expect(wrapper.find("th.pos-orders-table-header--vehicles.pos-orders-table-header--compact").exists()).toBe(true);
expect(wrapper.find("th .pleno-table-header-content").exists()).toBe(true);
setUseLargeTableHeaders(true);
await nextTick();
expect(wrapper.find("th.pos-orders-table-header--compact").exists()).toBe(false);
expect(wrapper.find("th.pos-orders-table-header--vehicles").exists()).toBe(true);
});
it("renders stacked desktop registration numbers in the compact vehicle column style", async () => {
const wrapper = mountOrdersTable({
invoiceView: true,
orders: [
createOrder({
reg_1: "AB12345",
reg_2: "CD67890",
reg_3: "EF24680",
}),
],
});
await flushPromises();
const vehicleCell = wrapper.get(".pos-order-vehicle-cell");
const registrationLines = vehicleCell.findAll(".pos-order-vehicle-registration-line");
expect(registrationLines).toHaveLength(3);
expect(registrationLines.map((line) => line.text())).toEqual(["AB12345", "CD67890", "EF24680"]);
});
it("centers the desktop ID status indicator group in the ID cell", async () => {
const wrapper = mountOrdersTable({
invoiceView: true,
orders: [createOrder({ id: 54518 })],
});
await flushPromises();
const idCell = wrapper.get(".pos-orders-table-id-cell");
expect(idCell.classes()).toContain("is-narrow");
expect(idCell.text()).toContain("54518");
expect(idCell.find(".pos-orders-table-id-cell-content").exists()).toBe(true);
expect(idCell.find(".color-indicator-stub").exists()).toBe(true);
});
it("uses row-level alignment classes for the desktop orders table row", async () => {
const wrapper = mountOrdersTable({
invoiceView: true,
orders: [createOrder({ id: 54518 })],
});
await flushPromises();
const row = wrapper.get(".pos-orders-table-row");
expect(row.find(".pos-orders-table-id-cell").exists()).toBe(true);
expect(row.find(".pos-order-vehicle-cell").exists()).toBe(true);
expect(row.find(".pos-orders-table-actions-cell").exists()).toBe(true);
});
});
@@ -0,0 +1,34 @@
// @vitest-environment jsdom
import { beforeEach, describe, expect, it } from "vitest";
import {
LARGE_TABLE_HEADERS_BODY_CLASS,
LARGE_TABLE_HEADERS_STORAGE_KEY,
setUseLargeTableHeaders,
useLargeTableHeaders,
} from "@/services/tableHeaderPreferences.js";
describe("table header preferences", () => {
beforeEach(() => {
window.localStorage.removeItem(LARGE_TABLE_HEADERS_STORAGE_KEY);
setUseLargeTableHeaders(false);
});
it("uses compact table headers by default", () => {
expect(useLargeTableHeaders.value).toBe(false);
expect(document.body.classList.contains(LARGE_TABLE_HEADERS_BODY_CLASS)).toBe(false);
});
it("toggles the document class for large table headers", () => {
setUseLargeTableHeaders(true);
expect(window.localStorage.getItem(LARGE_TABLE_HEADERS_STORAGE_KEY)).toBe("true");
expect(useLargeTableHeaders.value).toBe(true);
expect(document.body.classList.contains(LARGE_TABLE_HEADERS_BODY_CLASS)).toBe(true);
setUseLargeTableHeaders(false);
expect(window.localStorage.getItem(LARGE_TABLE_HEADERS_STORAGE_KEY)).toBe("false");
expect(useLargeTableHeaders.value).toBe(false);
expect(document.body.classList.contains(LARGE_TABLE_HEADERS_BODY_CLASS)).toBe(false);
});
});