- Added an audit trail feature that logs every create, update, and delete action on delivery notes and orders, capturing detailed changes per field. - Introduced the `AuditTimeline` component to display the history of changes for delivery notes and orders, enhancing traceability. - Updated the `page.tsx` files for delivery notes and orders to fetch and display audit logs, ensuring users can view the history of modifications. - Enhanced the `package.json` with a new migration script for setting up the audit log functionality. This feature improves accountability and transparency in the management of delivery notes and orders.
244 lines
7.8 KiB
TypeScript
244 lines
7.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
||
import {
|
||
buildBuckets,
|
||
periodDays,
|
||
periodLabel,
|
||
resolvePeriod,
|
||
metricsTotal,
|
||
type RepMetrics,
|
||
} from "./rep-performance-service";
|
||
|
||
/**
|
||
* These cover the pure date maths behind the rep-performance report. Every
|
||
* case here is a bug that actually shipped once:
|
||
* - bucket labels rendered "01 Sept – 31 Aug" (double off-by-one),
|
||
* - single-day buckets ended at midnight so a 05:46 record fell outside
|
||
* its own bucket and the whole report read zero,
|
||
* - a Jul–Sep range collapsed its label to "July 2026".
|
||
*/
|
||
describe("resolvePeriod", () => {
|
||
const anchor = "2026-09-15"; // a Tuesday
|
||
|
||
it("defaults to the current month when nothing is supplied", () => {
|
||
const p = resolvePeriod({ now: new Date("2026-09-28T10:00:00.000Z") });
|
||
expect(p.preset).toBe("month");
|
||
expect(p.from).toBe("2026-09-01");
|
||
expect(p.to).toBe("2026-09-30");
|
||
expect(p.granularity).toBe("week");
|
||
});
|
||
|
||
it("day preset covers exactly that day", () => {
|
||
const p = resolvePeriod({ preset: "day", anchor, now: new Date("2026-09-28T10:00:00.000Z") });
|
||
expect(p.from).toBe("2026-09-15");
|
||
expect(p.to).toBe("2026-09-15");
|
||
expect(p.granularity).toBe("day");
|
||
});
|
||
|
||
it("week preset is Monday-anchored", () => {
|
||
const p = resolvePeriod({ preset: "week", anchor, now: new Date("2026-09-28T10:00:00.000Z") });
|
||
expect(p.from).toBe("2026-09-14");
|
||
expect(p.to).toBe("2026-09-20");
|
||
expect(p.granularity).toBe("day");
|
||
});
|
||
|
||
it("month preset ignores the day of the anchor", () => {
|
||
const p = resolvePeriod({ preset: "month", anchor, now: new Date("2026-09-28T10:00:00.000Z") });
|
||
expect(p.from).toBe("2026-09-01");
|
||
expect(p.to).toBe("2026-09-30");
|
||
});
|
||
|
||
it("quarter preset snaps to the containing quarter", () => {
|
||
const p = resolvePeriod({ preset: "quarter", anchor, now: new Date("2026-09-28T10:00:00.000Z") });
|
||
expect(p.from).toBe("2026-07-01");
|
||
expect(p.to).toBe("2026-09-30");
|
||
expect(p.granularity).toBe("month");
|
||
});
|
||
|
||
it("year preset is the whole calendar year", () => {
|
||
const p = resolvePeriod({ preset: "year", anchor, now: new Date("2026-09-28T10:00:00.000Z") });
|
||
expect(p.from).toBe("2026-01-01");
|
||
expect(p.to).toBe("2026-12-31");
|
||
});
|
||
|
||
it("custom preset honours from/to", () => {
|
||
const p = resolvePeriod({
|
||
preset: "custom",
|
||
from: "2026-08-01",
|
||
to: "2026-08-31",
|
||
now: new Date("2026-09-28T10:00:00.000Z"),
|
||
});
|
||
expect(p.from).toBe("2026-08-01");
|
||
expect(p.to).toBe("2026-08-31");
|
||
expect(p.granularity).toBe("week");
|
||
});
|
||
|
||
it("custom with a reversed range is normalised rather than rejected", () => {
|
||
const p = resolvePeriod({
|
||
preset: "custom",
|
||
from: "2026-08-31",
|
||
to: "2026-08-01",
|
||
now: new Date("2026-09-28T10:00:00.000Z"),
|
||
});
|
||
expect(p.from).toBe("2026-08-31");
|
||
expect(p.to).toBe("2026-08-31");
|
||
});
|
||
|
||
it("falls back to the anchor month when custom is half-specified", () => {
|
||
const p = resolvePeriod({
|
||
preset: "custom",
|
||
from: "2026-08-01",
|
||
now: new Date("2026-09-28T10:00:00.000Z"),
|
||
});
|
||
expect(p.from).toBe("2026-09-01");
|
||
expect(p.to).toBe("2026-09-30");
|
||
});
|
||
|
||
it("rejects junk rather than throwing", () => {
|
||
const p = resolvePeriod({
|
||
preset: "nonsense" as never,
|
||
from: "not-a-date",
|
||
to: "2026-13-45",
|
||
granularity: "hourly" as never,
|
||
now: new Date("2026-09-28T10:00:00.000Z"),
|
||
});
|
||
expect(p.preset).toBe("month");
|
||
expect(p.granularity).toBe("week");
|
||
expect(p.from).toBe("2026-09-01");
|
||
});
|
||
|
||
it("explicit granularity always wins over the preset default", () => {
|
||
const p = resolvePeriod({ preset: "month", granularity: "day", anchor });
|
||
expect(p.granularity).toBe("day");
|
||
});
|
||
|
||
it("clamps an absurd custom range instead of scanning years of rows", () => {
|
||
const p = resolvePeriod({
|
||
preset: "custom",
|
||
from: "2000-01-01",
|
||
to: "2026-09-30",
|
||
now: new Date("2026-09-28T10:00:00.000Z"),
|
||
});
|
||
expect(periodDays(p)).toBe(401); // capped at 400 days + the inclusive end
|
||
});
|
||
});
|
||
|
||
describe("buildBuckets", () => {
|
||
const september = { preset: "month" as const, from: "2026-09-01", to: "2026-09-30" };
|
||
|
||
it("weekly buckets are contiguous, exhaustive and start at the window start", () => {
|
||
const buckets = buildBuckets({ ...september, granularity: "week" });
|
||
expect(buckets[0].start).toBe("2026-09-01");
|
||
expect(buckets.at(-1)!.end).toBe("2026-09-30");
|
||
for (let i = 1; i < buckets.length; i++) {
|
||
const prev = new Date(`${buckets[i - 1].end}T00:00:00.000Z`).getTime();
|
||
const next = new Date(`${buckets[i].start}T00:00:00.000Z`).getTime();
|
||
expect(next - prev).toBe(86_400_000); // no gap, no overlap
|
||
}
|
||
});
|
||
|
||
it("bucket labels never end before they start", () => {
|
||
// Regression: labels used to render "01 Sept – 31 Aug".
|
||
for (const granularity of ["day", "week", "month"] as const) {
|
||
for (const b of buildBuckets({ ...september, granularity })) {
|
||
expect(b.start <= b.end).toBe(true);
|
||
}
|
||
}
|
||
});
|
||
|
||
it("monthly fold of a month range is a single bucket", () => {
|
||
const buckets = buildBuckets({ ...september, granularity: "month" });
|
||
expect(buckets).toHaveLength(1);
|
||
expect(buckets[0]).toMatchObject({ start: "2026-09-01", end: "2026-09-30" });
|
||
});
|
||
|
||
it("daily fold yields one bucket per day in the window", () => {
|
||
const buckets = buildBuckets({ ...september, granularity: "day" });
|
||
expect(buckets).toHaveLength(30);
|
||
expect(buckets[0]).toMatchObject({ start: "2026-09-01", end: "2026-09-01" });
|
||
expect(buckets.at(-1)).toMatchObject({ start: "2026-09-30", end: "2026-09-30" });
|
||
});
|
||
|
||
it("handles a single-day period", () => {
|
||
const buckets = buildBuckets({
|
||
preset: "day",
|
||
from: "2026-09-15",
|
||
to: "2026-09-15",
|
||
granularity: "day",
|
||
});
|
||
expect(buckets).toHaveLength(1);
|
||
});
|
||
|
||
it("returns nothing for unparseable dates", () => {
|
||
expect(
|
||
buildBuckets({ preset: "custom", from: "nope", to: "also-nope", granularity: "day" }),
|
||
).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe("periodLabel", () => {
|
||
it("names a whole month", () => {
|
||
expect(
|
||
periodLabel({ preset: "month", from: "2026-09-01", to: "2026-09-30", granularity: "week" }),
|
||
).toBe("September 2026");
|
||
});
|
||
|
||
it("names a single day", () => {
|
||
expect(
|
||
periodLabel({ preset: "day", from: "2026-09-15", to: "2026-09-15", granularity: "day" }),
|
||
).toBe("15 September 2026");
|
||
});
|
||
|
||
it("names a whole week", () => {
|
||
expect(
|
||
periodLabel({ preset: "week", from: "2026-09-14", to: "2026-09-20", granularity: "day" }),
|
||
).toBe("Week of 14 September 2026");
|
||
});
|
||
|
||
it("does not collapse a multi-month range to its first month", () => {
|
||
// Regression: a Jul–Sep range used to report "July 2026".
|
||
const label = periodLabel({
|
||
preset: "custom",
|
||
from: "2026-07-01",
|
||
to: "2026-09-30",
|
||
granularity: "month",
|
||
});
|
||
expect(label).not.toBe("July 2026");
|
||
expect(label).toContain("1 July");
|
||
expect(label).toContain("30 September 2026");
|
||
});
|
||
|
||
it("spells out an arbitrary range", () => {
|
||
const label = periodLabel({
|
||
preset: "custom",
|
||
from: "2026-09-03",
|
||
to: "2026-09-19",
|
||
granularity: "week",
|
||
});
|
||
expect(label).toBe("3 September – 19 September 2026");
|
||
});
|
||
});
|
||
|
||
describe("metricsTotal", () => {
|
||
it("sums every metric", () => {
|
||
const m: RepMetrics = {
|
||
quotesLoaded: 1,
|
||
quotesCompleted: 2,
|
||
ordersPlaced: 3,
|
||
ordersReceived: 4,
|
||
deliveryNotesRaised: 5,
|
||
};
|
||
expect(metricsTotal(m)).toBe(15);
|
||
});
|
||
|
||
it("is zero for an all-zero row", () => {
|
||
expect(
|
||
metricsTotal({
|
||
quotesLoaded: 0,
|
||
quotesCompleted: 0,
|
||
ordersPlaced: 0,
|
||
ordersReceived: 0,
|
||
deliveryNotesRaised: 0,
|
||
}),
|
||
).toBe(0);
|
||
});
|
||
});
|