Add E2E and unit tests for invoicing period flag handling, enhance duplicate groups filtering by date, and implement Excel export for department 75 distribution
This commit is contained in:
+74
-1
@@ -8,6 +8,8 @@ import {view} from "../../imports/InvoicingBillingPeriodImportView.vue";
|
||||
import {departments} from "@/components/pagination/departmentTabs.vue";
|
||||
import {SessionUser} from "@/components/session/token/SessionUser.vue";
|
||||
import {BTooltip} from "buefy";
|
||||
import { exportRowsToExcel } from '@/services/TableExcelExportService.js';
|
||||
import { extractVisibleRowsFromTable } from '@/services/AutoTableExportService.js';
|
||||
import {
|
||||
getDepartmentRelativeBookedAmount as calculateDepartmentRelativeBookedAmount,
|
||||
getBookedDepartment75SourceBookedTotal,
|
||||
@@ -380,6 +382,24 @@ const hasDepartment75CombinedBasis = computed(() => {
|
||||
|| vehicleSubscriptionDistribution.value?.total_subscription_price !== undefined;
|
||||
});
|
||||
|
||||
const isDepartment75CombinedFullyLoaded = computed(() => {
|
||||
return fixed_pricing_department_distribution.value?.total_fixed_price !== undefined
|
||||
&& vehicleSubscriptionDistribution.value?.total_subscription_price !== undefined;
|
||||
});
|
||||
|
||||
const department75Table = ref<HTMLTableElement | null>(null);
|
||||
|
||||
const downloadExcel = () => {
|
||||
if (!department75Table.value) return;
|
||||
|
||||
// @ts-ignore
|
||||
const rows = extractVisibleRowsFromTable(department75Table.value);
|
||||
exportRowsToExcel(rows, {
|
||||
filename: `invoicing-period-department-75-${dates.computed.formattedStartDate.value}-${dates.computed.formattedEndDate.value}.xlsx`,
|
||||
sheetName: 'Department 75 Distribution'
|
||||
});
|
||||
};
|
||||
|
||||
const formatCurrencyAmount = (amount: any) => {
|
||||
return SessionUser.functions.currency.toLocal(toFiniteNumber(amount));
|
||||
};
|
||||
@@ -509,12 +529,14 @@ const formatCurrencyAmountOrDash = (amount: any) => {
|
||||
<div class="column is-full">
|
||||
<WhiteBoxCard :has-selection-style="true" :has-hover-effect="true" :has-border="true">
|
||||
<div
|
||||
class="table-container invoicing-period-distribution-table-container"
|
||||
class="table-container invoicing-period-distribution-table-container mb-0"
|
||||
data-testid="department-75-combined-distribution-table-container"
|
||||
data-disable-auto-excel-export="1"
|
||||
>
|
||||
<table
|
||||
class="table is-fullwidth is-striped is-hoverable is-bordered is-narrow invoicing-period-distribution-table"
|
||||
data-testid="department-75-combined-distribution-table"
|
||||
ref="department75Table"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -576,6 +598,57 @@ const formatCurrencyAmountOrDash = (amount: any) => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="mt-0 has-text-right">
|
||||
<!-- If any of the department 75 data is not fully loaded, show the loading status buttons -->
|
||||
<template v-if="!isDepartment75CombinedFullyLoaded">
|
||||
<div class="columns is-mobile">
|
||||
<!-- Loading state: 1. Fixed pricing distribution loading -->
|
||||
<div class="column is-one-third">
|
||||
<button class="button is-dark is-small is-fullwidth is-justify-content-space-between has-text-white" :class="{'is-active': fixed_pricing_department_distribution?.total_fixed_price === undefined}">
|
||||
<!-- [Label] [Icon pulled right] -->
|
||||
<span>Fast-pris fordeling</span>
|
||||
|
||||
<span class="icon is-small">
|
||||
<i class="fas" :class="fixed_pricing_department_distribution?.total_fixed_price === undefined ? 'fa-spinner fa-spin' : 'fa-check'"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Loading state: 2. Vehicle subscription distribution loading -->
|
||||
<div class="column is-one-third">
|
||||
<button class="button is-dark is-small is-fullwidth is-justify-content-space-between has-text-white" :class="{'is-active': vehicleSubscriptionDistribution?.total_subscription_price === undefined}">
|
||||
<!-- [Label] [Icon pulled right] -->
|
||||
<span>Vaske-aftale fordeling</span>
|
||||
|
||||
<span class="icon is-small">
|
||||
<i class="fas" :class="vehicleSubscriptionDistribution?.total_subscription_price === undefined ? 'fa-spinner fa-spin' : 'fa-check'"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Loading state: 3. Booked department 75 distribution loading -->
|
||||
<div class="column is-one-third">
|
||||
<button class="button is-dark is-small is-fullwidth is-justify-content-space-between has-text-white" :class="{'is-active': !bookedDepartment75DistributionLoaded || bookedDepartment75DistributionFailed}">
|
||||
<!-- [Label] [Icon pulled right] -->
|
||||
<span>Afdeling 75 bogført fordeling</span>
|
||||
<span class="icon is-small">
|
||||
<i class="fas" :class="!bookedDepartment75DistributionLoaded || bookedDepartment75DistributionFailed ? 'fa-spinner fa-spin' : 'fa-check'"></i>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<button
|
||||
class="button is-dark is-small is-fullwidth"
|
||||
:class="{'is-loading': !isDepartment75CombinedFullyLoaded}"
|
||||
@click="downloadExcel"
|
||||
v-else
|
||||
>
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-file-excel"></i>
|
||||
</span>
|
||||
<span>Download Excel</span>
|
||||
</button>
|
||||
</div>
|
||||
</WhiteBoxCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
+2
@@ -386,6 +386,7 @@ const getPeriodRequestParameters = () => ({
|
||||
page: periodPaging.page,
|
||||
limit: periodPaging.limit,
|
||||
search: periodPaging.search.trim(),
|
||||
flagTab: periodPaging.flagTab,
|
||||
includeRequiresAction: periodPaging.includeRequiresAction ? 1 : 0,
|
||||
includeBooked: periodPaging.includeBooked ? 1 : 0,
|
||||
});
|
||||
@@ -1003,6 +1004,7 @@ watch(
|
||||
periodPaging.page,
|
||||
periodPaging.limit,
|
||||
periodPaging.search,
|
||||
periodPaging.flagTab,
|
||||
periodPaging.includeRequiresAction,
|
||||
periodPaging.includeBooked,
|
||||
],
|
||||
|
||||
+4
@@ -24,6 +24,7 @@ export const periodPaging = reactive({
|
||||
page: 1,
|
||||
limit: DEFAULT_LIMIT,
|
||||
search: "",
|
||||
flagTab: "all",
|
||||
includeRequiresAction: true,
|
||||
includeBooked: true,
|
||||
meta: defaultMeta(),
|
||||
@@ -82,6 +83,7 @@ export const resetPeriodPagingState = ({ clearCache = true } = {}) => {
|
||||
periodPaging.page = 1;
|
||||
periodPaging.limit = DEFAULT_LIMIT;
|
||||
periodPaging.search = "";
|
||||
periodPaging.flagTab = "all";
|
||||
periodPaging.includeRequiresAction = true;
|
||||
periodPaging.includeBooked = true;
|
||||
periodPaging.meta = defaultMeta();
|
||||
@@ -125,6 +127,7 @@ export const buildPeriodCacheKey = ({
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
flagTab,
|
||||
includeRequiresAction,
|
||||
includeBooked,
|
||||
}) =>
|
||||
@@ -135,6 +138,7 @@ export const buildPeriodCacheKey = ({
|
||||
normalizePage(page),
|
||||
normalizeLimit(limit),
|
||||
String(search || "").trim(),
|
||||
String(flagTab || "all").trim(),
|
||||
includeRequiresAction ? "1" : "0",
|
||||
includeBooked ? "1" : "0",
|
||||
]
|
||||
|
||||
+187
-7
@@ -156,6 +156,80 @@ const customersInCurrentView = computed(
|
||||
() => view.variables.sharedVariables.value?.types?.[view.computed.componentName.value] ?? []
|
||||
);
|
||||
|
||||
type PeriodFlagTab = "all" | "red" | "yellow" | "none" | "filters";
|
||||
type CustomerFlagTab = "red" | "yellow" | "none";
|
||||
|
||||
const selectedFlagTab = computed<PeriodFlagTab>({
|
||||
get: () => (periodPaging.flagTab as PeriodFlagTab) || "all",
|
||||
set: (value) => {
|
||||
if (periodPaging.flagTab !== value) {
|
||||
periodPaging.flagTab = value;
|
||||
periodPaging.page = 1;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getCustomerFlagTabType = (customer: any): CustomerFlagTab => {
|
||||
const flagCounts = getCustomerActiveFlagCounts(customer);
|
||||
if (flagCounts.manual > 0) {
|
||||
return "red";
|
||||
}
|
||||
|
||||
if (flagCounts.automatic > 0) {
|
||||
return "yellow";
|
||||
}
|
||||
|
||||
return "none";
|
||||
};
|
||||
|
||||
const toNonNegativeInteger = (value: any) => {
|
||||
const parsed = Number.parseInt(String(value ?? "0"), 10);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : 0;
|
||||
};
|
||||
|
||||
const periodFlagTabCounts = computed(() => {
|
||||
const currentTypeName = view.computed.componentName.value;
|
||||
const backendCounts = periodPaging.typeCounts?.[currentTypeName];
|
||||
if (backendCounts && typeof backendCounts === "object") {
|
||||
const all = toNonNegativeInteger(backendCounts.total);
|
||||
const red = Math.min(all, toNonNegativeInteger(backendCounts.manual_flags));
|
||||
const yellow = Math.min(Math.max(0, all - red), toNonNegativeInteger(backendCounts.automatic_flags));
|
||||
return {
|
||||
all,
|
||||
red,
|
||||
yellow,
|
||||
none: Math.max(0, all - red - yellow),
|
||||
};
|
||||
}
|
||||
|
||||
const counts = { all: 0, red: 0, yellow: 0, none: 0 };
|
||||
customersInCurrentView.value.forEach((customer: any) => {
|
||||
counts.all += 1;
|
||||
counts[getCustomerFlagTabType(customer)] += 1;
|
||||
});
|
||||
return counts;
|
||||
});
|
||||
|
||||
const filteredCustomersInCurrentView = computed(() => {
|
||||
return customersInCurrentView.value;
|
||||
});
|
||||
|
||||
const getPeriodFlagTabIconClass = (tab: PeriodFlagTab) => {
|
||||
if (tab === "red") {
|
||||
return "has-text-danger";
|
||||
}
|
||||
|
||||
if (tab === "yellow") {
|
||||
return "has-text-warning";
|
||||
}
|
||||
|
||||
if (tab === "none") {
|
||||
return "has-text-success";
|
||||
}
|
||||
|
||||
return "has-text-grey-dark";
|
||||
};
|
||||
|
||||
const isPossibleDuplicatesView = computed(() => view.variables.currentView.value === "possible_duplicates");
|
||||
|
||||
const possibleDuplicateGroupsInCurrentView = computed(() =>
|
||||
@@ -227,6 +301,16 @@ const getTransactionIds = (customer: any) => {
|
||||
.map((transaction: any) => transaction.id);
|
||||
};
|
||||
|
||||
const getAllTransactionIds = (customer: any) => {
|
||||
if (!customer.transactions || customer.transactions.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return customer.transactions
|
||||
.map((transaction: any) => transaction.id)
|
||||
.filter((transactionId: any) => transactionId !== null && transactionId !== undefined);
|
||||
};
|
||||
|
||||
const getExcludedTransactionIds = (customer: any) => {
|
||||
if (!customer.transactions || customer.transactions.length === 0) {
|
||||
return [];
|
||||
@@ -513,7 +597,7 @@ const isFlagInCustomerCardScope = (customer: any, flag: any) => {
|
||||
return Number(flag?.customer_number || flag?.target_id || 0) === Number(customer?.customer_number || 0);
|
||||
}
|
||||
|
||||
const transactionIds = getTransactionIds(customer);
|
||||
const transactionIds = getAllTransactionIds(customer);
|
||||
const transactionIdSet = toPositiveIntegerSet(transactionIds);
|
||||
|
||||
if (["order", "order_field", "order_item", "order_item_field"].includes(targetType)) {
|
||||
@@ -672,12 +756,12 @@ const getTransactionQueryParameters = () => {
|
||||
show_fixed_pricing: true,
|
||||
};
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-testid="invoicing-period-view-all" :data-current-view="view.variables.currentView.value">
|
||||
<InvoicingBillingPeriodStatistics />
|
||||
<InvoicingBillingPeriodFilters @filterChanged="(filters) => onFilterChanged(filters)" />
|
||||
<div
|
||||
class="columns is-vcentered is-multiline pagination-general-search-reload"
|
||||
data-testid="invoicing-period-page-controls"
|
||||
@@ -724,8 +808,7 @@ const getTransactionQueryParameters = () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-2">
|
||||
<div v-if="isPossibleDuplicatesView" class="py-2">
|
||||
<div class="columns is-vcentered is-multiline is-mobile">
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label is-small">Per page</label>
|
||||
@@ -746,6 +829,93 @@ const getTransactionQueryParameters = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!isPossibleDuplicatesView" class="py-2">
|
||||
<b-tabs
|
||||
v-model="selectedFlagTab"
|
||||
expanded
|
||||
:class="['period-flag-tabs', { 'is-filters-tab-selected': selectedFlagTab === 'filters' }]"
|
||||
data-testid="invoicing-period-flag-tabs"
|
||||
>
|
||||
<b-tab-item value="all">
|
||||
<template #header>
|
||||
<span class="period-flag-tab-header">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-flag" :class="getPeriodFlagTabIconClass('all')"></i>
|
||||
</span>
|
||||
<span>All</span>
|
||||
<span class="tag is-light is-rounded">{{ periodFlagTabCounts.all }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item value="red">
|
||||
<template #header>
|
||||
<span class="period-flag-tab-header">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-flag" :class="getPeriodFlagTabIconClass('red')"></i>
|
||||
</span>
|
||||
<span>Red flags</span>
|
||||
<span class="tag is-light is-rounded">{{ periodFlagTabCounts.red }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item value="yellow">
|
||||
<template #header>
|
||||
<span class="period-flag-tab-header">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-flag" :class="getPeriodFlagTabIconClass('yellow')"></i>
|
||||
</span>
|
||||
<span>Yellow flags</span>
|
||||
<span class="tag is-light is-rounded">{{ periodFlagTabCounts.yellow }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item value="none">
|
||||
<template #header>
|
||||
<span class="period-flag-tab-header">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-flag" :class="getPeriodFlagTabIconClass('none')"></i>
|
||||
</span>
|
||||
<span>No flags</span>
|
||||
<span class="tag is-light is-rounded">{{ periodFlagTabCounts.none }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item value="filters" class="filters-tab" data-testid="invoicing-period-filters-tab">
|
||||
<template #header>
|
||||
<span class="period-flag-tab-header">
|
||||
<span class="icon is-small">
|
||||
<i class="fas fa-filter"></i>
|
||||
</span>
|
||||
<span>Filters</span>
|
||||
</span>
|
||||
</template>
|
||||
<div class="period-flag-tab-content py-2">
|
||||
<div class="columns is-vcentered is-multiline is-mobile" data-testid="invoicing-period-table-filters">
|
||||
<div class="column is-narrow my-3">
|
||||
<label class="label is-small">Per page</label>
|
||||
<div class="control">
|
||||
<div class="select">
|
||||
<select
|
||||
:value="periodPaging.limit"
|
||||
data-testid="invoicing-period-limit-select"
|
||||
@change="onPeriodLimitChange"
|
||||
>
|
||||
<option v-for="limit in PERIOD_LIMIT_OPTIONS" :key="limit" :value="limit">
|
||||
{{ limit === "all" ? "All" : limit }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InvoicingBillingPeriodFilters @filterChanged="(filters) => onFilterChanged(filters)" />
|
||||
</div>
|
||||
</b-tab-item>
|
||||
</b-tabs>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-if="isPossibleDuplicatesView" class="columns is-multiline is-mobile">
|
||||
<template v-for="duplicateGroup in possibleDuplicateGroupsInCurrentView" :key="duplicateGroup.key">
|
||||
<div class="column is-12">
|
||||
@@ -845,8 +1015,8 @@ const getTransactionQueryParameters = () => {
|
||||
:limit-results="false"
|
||||
:hide-pagination="true"
|
||||
:dates="{
|
||||
dateFrom: dates.computed.formattedStartDate.value,
|
||||
dateTo: dates.computed.formattedEndDate.value,
|
||||
dateFrom: duplicateGroup.dateKey || dates.computed.formattedStartDate.value,
|
||||
dateTo: duplicateGroup.dateKey || dates.computed.formattedEndDate.value,
|
||||
}"
|
||||
:show-only-with-ids="getDuplicateGroupTransactionIds(duplicateGroup)"
|
||||
:query-parameters="getTransactionQueryParameters()"
|
||||
@@ -861,7 +1031,7 @@ const getTransactionQueryParameters = () => {
|
||||
</div>
|
||||
|
||||
<div v-else class="columns is-multiline is-mobile">
|
||||
<template v-for="customer in customersInCurrentView" :key="customer.customer_number">
|
||||
<template v-for="customer in filteredCustomersInCurrentView" :key="customer.customer_number">
|
||||
<div class="column is-12">
|
||||
<WhiteBox
|
||||
class="mb-2"
|
||||
@@ -1195,6 +1365,16 @@ const getTransactionQueryParameters = () => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.period-flag-tabs:not(.is-filters-tab-selected) :deep(.tab-content) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.period-flag-tab-header {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.duplicate-comparison-row {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@@ -2226,6 +2226,7 @@ test.describe("Invoicing period tab", () => {
|
||||
await page.getByTestId("invoicing-period-view-selector-all").click();
|
||||
await expect(page.getByTestId("invoicing-period-customer-9101")).toBeVisible();
|
||||
await expect(page.getByTestId("invoicing-period-view-selector-all")).toContainText("(0/30)");
|
||||
await page.getByTestId("invoicing-period-flag-tabs").getByText("Filters").first().click();
|
||||
await expect(page.getByTestId("invoicing-period-limit-select")).toHaveValue("100");
|
||||
await expect.poll(() => periodRequests.some((request) => request.limit === "100")).toBeTruthy();
|
||||
const statistics = page.getByTestId("invoicing-period-statistics");
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("InvoiceOrdersPagination uses specific date for duplicate groups", async ({ page }) => {
|
||||
// We don't need full auth for this check if we mock the right things
|
||||
|
||||
const mockPeriodData = {
|
||||
possible_duplicates: [
|
||||
{
|
||||
key: "ABC1234|2026-05-12",
|
||||
plate: "ABC1234",
|
||||
dateKey: "2026-05-12",
|
||||
testId: "ABC1234-2026-05-12",
|
||||
customers: [
|
||||
{
|
||||
customer_number: 1001,
|
||||
customer_name: "Customer A",
|
||||
transactions: [{ id: 1, plate: "ABC1234", date: "2026-05-12T10:00:00Z" }],
|
||||
},
|
||||
{
|
||||
customer_number: 1002,
|
||||
customer_name: "Customer B",
|
||||
transactions: [{ id: 2, plate: "ABC1234", date: "2026-05-12T11:00:00Z" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
types: { all: [] },
|
||||
statistics: { total_amount: 0, total_count: 0 },
|
||||
};
|
||||
|
||||
await page.route("**/superuser/invoicing/period**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(mockPeriodData),
|
||||
});
|
||||
});
|
||||
|
||||
let capturedOrdersUrl = null;
|
||||
await page.route("**/orders**", async (route) => {
|
||||
if (route.request().method() === "GET" && route.request().url().includes("orders")) {
|
||||
capturedOrdersUrl = route.request().url();
|
||||
}
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ data: [], meta: { total: 0 } }),
|
||||
});
|
||||
});
|
||||
|
||||
// Mock initial session data
|
||||
await page.route("**/auth/session**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ data: { user: { role: "superuser" } } }),
|
||||
});
|
||||
});
|
||||
|
||||
// Navigate directly to the view
|
||||
await page.goto(
|
||||
"/superuser/invoices?activeTab=period&periodView=possible_duplicates&startDate=2026-05-01&endDate=2026-05-31"
|
||||
);
|
||||
|
||||
// Wait for the duplicate group and click it
|
||||
const duplicateGroup = page.getByTestId("invoicing-period-duplicate-group-ABC1234-2026-05-12");
|
||||
await expect(duplicateGroup).toBeVisible();
|
||||
await duplicateGroup.click();
|
||||
|
||||
// Wait for the orders request
|
||||
await page.waitForResponse((res) => res.url().includes("/orders") && res.request().method() === "GET");
|
||||
|
||||
console.log("Captured Orders URL:", capturedOrdersUrl);
|
||||
|
||||
// Verify the date range in the URL
|
||||
expect(capturedOrdersUrl).toContain("created_at-date_from=2026-05-12");
|
||||
expect(capturedOrdersUrl).toContain("created_at-date_to=2026-05-12");
|
||||
});
|
||||
@@ -157,6 +157,13 @@ const mountView = () =>
|
||||
mount(InvoicingBillingPeriodViewAll, {
|
||||
global: {
|
||||
stubs: {
|
||||
"b-tabs": {
|
||||
template: "<div data-testid='b-tabs-stub'><slot /></div>",
|
||||
},
|
||||
"b-tab-item": {
|
||||
template:
|
||||
"<div class='b-tab-item-stub' v-bind='$attrs'><div class='b-tab-item-header'><slot name='header' /></div><slot /></div>",
|
||||
},
|
||||
InvoicingBillingPeriodStatistics: { template: "<div />" },
|
||||
InvoicingBillingPeriodFilters: { template: "<div />" },
|
||||
PaginationNavigation: {
|
||||
@@ -346,6 +353,42 @@ describe("Invoicing period queue state", () => {
|
||||
expect(periodPaging.limit).toBe(500);
|
||||
});
|
||||
|
||||
it("stores table filters in the Filters tab for the standard view", async () => {
|
||||
const wrapper = mountView();
|
||||
await nextTick();
|
||||
|
||||
const filtersTab = wrapper.get("[data-testid='invoicing-period-filters-tab']");
|
||||
expect(filtersTab.find("[data-testid='invoicing-period-table-filters']").exists()).toBe(true);
|
||||
expect(filtersTab.find("[data-testid='invoicing-period-limit-select']").exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps flag tab counts stable when only the page size changes", async () => {
|
||||
periodPaging.typeCounts = {
|
||||
all: {
|
||||
total: 793,
|
||||
manual_flags: 2,
|
||||
automatic_flags: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mountView();
|
||||
await nextTick();
|
||||
|
||||
const getFlagTabCounts = () =>
|
||||
wrapper
|
||||
.findAll("[data-testid='invoicing-period-flag-tabs'] .period-flag-tab-header .tag")
|
||||
.map((tag) => Number.parseInt(tag.text(), 10));
|
||||
|
||||
expect(getFlagTabCounts()).toEqual([793, 2, 0, 791]);
|
||||
|
||||
const select = wrapper.get("[data-testid='invoicing-period-limit-select']");
|
||||
await select.setValue("10");
|
||||
await nextTick();
|
||||
|
||||
expect(periodPaging.limit).toBe(10);
|
||||
expect(getFlagTabCounts()).toEqual([793, 2, 0, 791]);
|
||||
});
|
||||
|
||||
it("keeps only one customer unfolded at a time", async () => {
|
||||
const wrapper = mountView();
|
||||
await nextTick();
|
||||
@@ -438,6 +481,55 @@ describe("Invoicing period queue state", () => {
|
||||
expect(customer.get("[data-testid='invoice-period-flags-stub']").text()).toBe("2");
|
||||
});
|
||||
|
||||
it("keeps collected invoice flags visible even when linked transactions are excluded", async () => {
|
||||
sharedVariablesRef.value = {
|
||||
types: {
|
||||
all: [
|
||||
{
|
||||
id: 7,
|
||||
customer_number: 1007,
|
||||
customer_name: "Collection Flag Customer",
|
||||
requires_action: false,
|
||||
status_indicator: "flag_red",
|
||||
flag_counts: {
|
||||
manual: 1,
|
||||
automatic: 0,
|
||||
total: 1,
|
||||
},
|
||||
flags: [
|
||||
{
|
||||
id: 992,
|
||||
source: "manual",
|
||||
status: "active",
|
||||
target_type: "collected_order_invoice",
|
||||
target_id: 5101,
|
||||
customer_number: 1007,
|
||||
reason: "Invoice collection needs review",
|
||||
},
|
||||
],
|
||||
transactions: [
|
||||
{
|
||||
id: 7201,
|
||||
amount: 95,
|
||||
booked: false,
|
||||
excluded: true,
|
||||
invoice_collection_id: 5101,
|
||||
date: "2026-04-14T10:00:00.000Z",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = mountView();
|
||||
await nextTick();
|
||||
|
||||
const customer = wrapper.get("[data-testid='invoicing-period-customer-1007']");
|
||||
expect(customer.get(".color-indicator-stub").attributes("data-icon")).toBe("fas fa-flag");
|
||||
expect(customer.get("[data-testid='invoice-period-flags-stub']").text()).toBe("1");
|
||||
});
|
||||
|
||||
it("removes the customer flag icon after the last active flag is marked inactive", async () => {
|
||||
sharedVariablesRef.value = {
|
||||
types: {
|
||||
|
||||
Reference in New Issue
Block a user