Restore invoicing period right rail layout (#249)

## What changed

- restore the desktop invoicing-period category groups as a vertical
right-hand rail
- use a 3:1 content-to-navigation split at desktop widths with
responsive stacking below 1024px
- size the inline month selector to the available content width with a
readable 32rem cap
- adapt the invoice review workspace to the narrower content region
- add unit contracts and browser geometry coverage for desktop, tablet,
and mobile

## Why

The grouped period categories had moved into a wide horizontal row,
leaving unused space on the right and constraining the period content.
The inline month picker was also too small to read comfortably.

## User impact

Superusers again see period categories in the right rail on desktop.
Compact layouts retain responsive navigation, and the review/object-tree
content avoids field wrapping when the main region becomes narrow.

## Validation

- 60/60 focused unit tests
- 30/30 full Chromium desktop invoicing-period browser tests
- 9/9 priority browser tests across Chromium desktop, tablet, and mobile
- ESLint
- production Vite build
- focused Prettier checks
- git diff check
This commit is contained in:
Jeppe B
2026-08-03 07:54:53 +00:00
committed by GitHub
parent f2453ba0a3
commit 1768f5a38e
6 changed files with 167 additions and 25 deletions
@@ -8,11 +8,11 @@
<div class="period-layout__top">
<slot name="content-top"></slot>
</div>
<div class="period-layout__navigation">
<aside class="period-layout__navigation" data-testid="invoicing-period-layout-navigation">
<slot name="summary"></slot>
<slot name="content-right"></slot>
</div>
<main class="period-layout__content">
</aside>
<main class="period-layout__content" data-testid="invoicing-period-layout-main">
<slot name="content-left"></slot>
</main>
<!-- Bottom column -->
@@ -26,13 +26,42 @@
.period-layout {
display: grid;
gap: 1rem;
grid-template-areas:
"top"
"navigation"
"content"
"bottom";
min-width: 0;
}
.period-layout__top,
.period-layout__navigation,
.period-layout__content,
.period-layout__top {
grid-area: top;
}
.period-layout__navigation {
align-self: start;
grid-area: navigation;
}
.period-layout__content {
grid-area: content;
}
.period-layout__bottom {
grid-area: bottom;
}
.period-layout > * {
min-width: 0;
}
@media screen and (min-width: 1024px) {
.period-layout {
grid-template-areas:
"top top"
"content navigation"
"bottom bottom";
grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);
}
}
</style>
@@ -1253,7 +1253,7 @@ watch(
.period-view-navigation {
display: grid;
gap: 0.75rem;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-columns: minmax(0, 1fr);
max-width: 100%;
min-width: 0;
width: 100%;
@@ -1290,7 +1290,7 @@ watch(
align-items: center;
display: grid;
gap: 0.35rem;
grid-template-columns: minmax(0, 1fr) minmax(88px, 132px);
grid-template-columns: minmax(0, 1fr) minmax(84px, 104px);
width: 100%;
}
@@ -1305,14 +1305,14 @@ watch(
display: flex;
flex: none;
justify-content: flex-end;
max-width: 132px;
min-width: 88px;
max-width: 104px;
min-width: 84px;
width: 100%;
}
.is-selector-view :deep(.period-selector-progress-slot) {
flex: 1 1 72px;
max-width: 100px;
max-width: 72px;
min-width: 56px;
width: auto;
}
@@ -1369,7 +1369,7 @@ watch(
background: #f6c344;
}
@media screen and (max-width: 1400px) {
@media screen and (min-width: 601px) and (max-width: 1023px) {
.period-view-navigation {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@@ -964,7 +964,11 @@ const getTransactionQueryParameters = () => {
</script>
<template>
<div data-testid="invoicing-period-view-all" :data-current-view="view.variables.currentView.value">
<div
class="invoicing-period-view-all"
data-testid="invoicing-period-view-all"
:data-current-view="view.variables.currentView.value"
>
<section
class="period-headline-totals"
:aria-label="tr('headline.aria_label', 'Periodens hovedtal')"
@@ -1613,6 +1617,10 @@ const getTransactionQueryParameters = () => {
</template>
<style scoped>
.invoicing-period-view-all {
container-type: inline-size;
}
.period-headline-totals {
display: grid;
gap: 0.75rem;
@@ -1881,6 +1889,13 @@ const getTransactionQueryParameters = () => {
.period-review-toolbar__field { flex: 1 1 160px; }
}
@container (max-width: 1024px) {
.period-review-workspace { grid-template-columns: 1fr; }
.period-review-queue { max-height: none; overflow: visible; }
.period-review-detail { order: -1; position: static; }
.period-review-detail__tree { max-height: none; overflow: visible; }
}
.duplicate-group-card {
overflow: visible;
}
@@ -14,14 +14,16 @@ const selectMonth = (month: Date | null) => {
<template>
<div class="invoicing-period-month-entry" data-testid="invoicing-period-month-entry">
<BuefyMonthField
v-model="selectedMonth"
value-type="date"
inline
:append-to-body="false"
data-testid="invoicing-period-inline-month-selector"
@change="selectMonth"
/>
<div class="invoicing-period-month-entry__selector">
<BuefyMonthField
v-model="selectedMonth"
value-type="date"
inline
:append-to-body="false"
data-testid="invoicing-period-inline-month-selector"
@change="selectMonth"
/>
</div>
</div>
</template>
@@ -33,4 +35,32 @@ const selectMonth = (month: Date | null) => {
min-height: 22rem;
padding: 1rem 0;
}
.invoicing-period-month-entry__selector {
max-width: 32rem;
width: 100%;
}
.invoicing-period-month-entry__selector :deep(.control),
.invoicing-period-month-entry__selector :deep(.datepicker),
.invoicing-period-month-entry__selector :deep(.dropdown),
.invoicing-period-month-entry__selector :deep(.dropdown-trigger),
.invoicing-period-month-entry__selector :deep(.dropdown-menu),
.invoicing-period-month-entry__selector :deep(.dropdown-content),
.invoicing-period-month-entry__selector :deep(.datepicker-header),
.invoicing-period-month-entry__selector :deep(.datepicker-table),
.invoicing-period-month-entry__selector :deep(.datepicker-months),
.invoicing-period-month-entry__selector :deep(.datepicker-table .datepicker-body),
.invoicing-period-month-entry__selector :deep(.datepicker-table .datepicker-body .datepicker-row) {
width: 100%;
}
.invoicing-period-month-entry__selector :deep(.datepicker-table .datepicker-body .datepicker-row) {
display: flex;
}
.invoicing-period-month-entry__selector :deep(.datepicker-table .datepicker-body .datepicker-cell) {
flex: 0 0 33.333%;
min-height: 3rem;
}
</style>
+29 -4
View File
@@ -1288,6 +1288,27 @@ test.describe("Invoicing period tab", () => {
await expect(page.getByTestId("date-period-start")).toHaveCount(0);
expect(selectedPeriodRequests).toHaveLength(0);
const mainRegion = page.getByTestId("invoicing-period-layout-main");
const navigationRegion = page.getByTestId("invoicing-period-layout-navigation");
const mainBox = await getBoundingBox(mainRegion, "period main region");
const navigationBox = await getBoundingBox(navigationRegion, "period navigation region");
const monthSelectorBox = await getBoundingBox(monthSelector, "inline month selector");
const viewportWidth = page.viewportSize()?.width ?? 0;
expectBoxInside(monthSelectorBox, mainBox, "inline month selector");
expect(monthSelectorBox.width).toBeGreaterThanOrEqual(Math.min(320, mainBox.width - 2));
expect(monthSelectorBox.width).toBeLessThanOrEqual(514);
if (viewportWidth >= 1024) {
expect(navigationBox.x).toBeGreaterThanOrEqual(mainBox.x + mainBox.width + 8);
expect(navigationBox.width / (mainBox.width + navigationBox.width)).toBeGreaterThanOrEqual(0.2);
expect(navigationBox.width / (mainBox.width + navigationBox.width)).toBeLessThanOrEqual(0.4);
} else {
expect(navigationBox.y + navigationBox.height).toBeLessThanOrEqual(mainBox.y + 1);
}
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth))
.toBeLessThanOrEqual(2);
const julyOption = monthSelector.locator(".datepicker-months .datepicker-cell").filter({ hasText: /jul/i });
await expect(julyOption).toHaveCount(1);
await julyOption.click();
@@ -1366,7 +1387,7 @@ test.describe("Invoicing period tab", () => {
const tabletQueueBox = await page.locator(".period-review-queue").boundingBox();
expect(tabletDetailBox).not.toBeNull();
expect(tabletQueueBox).not.toBeNull();
expect(tabletDetailBox.x).toBeGreaterThan(tabletQueueBox.x);
expect(tabletDetailBox.y).toBeLessThan(tabletQueueBox.y);
await expect
.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth))
.toBe(true);
@@ -2294,12 +2315,14 @@ test.describe("Invoicing period tab", () => {
const tableContainer = document.querySelector(
'[data-testid="department-75-combined-distribution-table-container"]'
);
const navigation = document.querySelector('[data-testid="invoicing-period-view-selectors"]');
if (!tableContainer || !navigation) {
const content = document.querySelector('[data-testid="invoicing-period-layout-main"]');
const navigation = document.querySelector('[data-testid="invoicing-period-layout-navigation"]');
if (!tableContainer || !content || !navigation) {
return false;
}
const tableContainerRect = tableContainer.getBoundingClientRect();
const contentRect = content.getBoundingClientRect();
const navigationRect = navigation.getBoundingClientRect();
const selectorRects = Array.from(navigation.querySelectorAll(".is-selector-view")).map((selector) =>
selector.getBoundingClientRect()
@@ -2308,7 +2331,9 @@ test.describe("Invoicing period tab", () => {
document.documentElement.scrollWidth - document.documentElement.clientWidth <= 2 &&
navigation.scrollWidth - navigation.clientWidth <= 2 &&
tableContainer.scrollWidth - tableContainer.clientWidth <= 2 &&
tableContainerRect.top >= navigationRect.bottom - 1 &&
tableContainerRect.left >= contentRect.left - 1 &&
tableContainerRect.right <= contentRect.right + 1 &&
contentRect.right <= navigationRect.left - 8 &&
selectorRects.every(
(rect) =>
rect.left >= navigationRect.left - 1 &&
@@ -38,6 +38,13 @@ const periodSource = readFileSync(
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/InvoicingBillingPeriod.vue"),
"utf8"
);
const periodLayoutSource = readFileSync(
join(
root,
"src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/InvoicingBillingPeriodLayoutDefault.vue"
),
"utf8"
);
const periodTopSource = readFileSync(
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/displays/layout/Top.vue"),
"utf8"
@@ -89,6 +96,10 @@ const periodViewAllSource = readFileSync(
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewAll.vue"),
"utf8"
);
const periodViewHomeSource = readFileSync(
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/views/InvoicingBillingPeriodViewHome.vue"),
"utf8"
);
const periodObjectTreeSource = readFileSync(
join(root, "src/views/dashboards/superUserDashboard/InvoicingBillingPeriod/components/InvoicingPeriodObjectTree.vue"),
"utf8"
@@ -409,6 +420,38 @@ describe("Periode tab contract", () => {
expect(periodSource).toContain("<template #content-bottom>");
});
it("places period content beside a three-to-one right navigation rail on desktop", () => {
expect(periodLayoutSource).toContain('data-testid="invoicing-period-layout-navigation"');
expect(periodLayoutSource).toContain('data-testid="invoicing-period-layout-main"');
expect(periodLayoutSource).toContain('"content navigation"');
expect(periodLayoutSource).toContain("grid-template-columns: minmax(0, 3fr) minmax(0, 1fr);");
expect(periodLayoutSource).toContain("@media screen and (min-width: 1024px)");
expect(periodLayoutSource).toContain('"navigation"');
expect(periodLayoutSource).toContain('"content"');
});
it("stacks grouped selectors in the desktop rail and keeps compact navigation responsive", () => {
expect(periodRightSource).toContain("grid-template-columns: minmax(0, 1fr);");
expect(periodRightSource).toContain("@media screen and (min-width: 601px) and (max-width: 1023px)");
expect(periodRightSource).toContain("grid-template-columns: repeat(2, minmax(0, 1fr));");
expect(periodRightSource).toContain("@media screen and (max-width: 600px)");
expect(periodRightSource).toContain("flex: 0 0 min(82vw, 320px);");
});
it("adapts the review workspace to the main region width created by the right rail", () => {
expect(periodViewAllSource).toContain('class="invoicing-period-view-all"');
expect(periodViewAllSource).toContain("container-type: inline-size;");
expect(periodViewAllSource).toContain("grid-template-columns: minmax(330px, 0.8fr) minmax(0, 1.6fr);");
expect(periodViewAllSource).toContain("@container (max-width: 1024px)");
});
it("sizes the inline month selector to the available content width with a readable cap", () => {
expect(periodViewHomeSource).toContain('class="invoicing-period-month-entry__selector"');
expect(periodViewHomeSource).toContain("max-width: 32rem;");
expect(periodViewHomeSource).toContain(":deep(.datepicker-months)");
expect(periodViewHomeSource).toContain("min-height: 3rem;");
});
it("keeps top selector and left dynamic view renderer", () => {
expect(periodTopSource).toContain("<InvoicingBillingPeriodDatePeriodSelector");
expect(periodTopSource).toContain("dates.variables.hasSelection.value");