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
+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");