New loader returns the sectioned shape the Gantt view needs: - buildPlanSections (pure): groups cards by project, sorts by start_date asc / title, splits subtasks into dated vs undated. Five unit tests cover the grouping and sorting rules. - loadPlan (server-only): one PocketBase trip per collection, delegates to buildPlanSections. 471/471 unit tests pass (was 466, +5 new).
197 lines
5.3 KiB
TypeScript
197 lines
5.3 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
|
|
import { buildPlanSections } from "@/lib/roadmap/load";
|
|
import type {
|
|
CardRecord,
|
|
ClientRecord,
|
|
ColumnRecord,
|
|
ProjectRecord,
|
|
SubtaskRecord,
|
|
} from "@/lib/pb/types";
|
|
|
|
function project(overrides: Partial<ProjectRecord> = {}): ProjectRecord {
|
|
return {
|
|
id: "p1",
|
|
created: "2026-01-01 00:00:00.000Z",
|
|
updated: "2026-01-01 00:00:00.000Z",
|
|
client: "c1",
|
|
name: "Client Board",
|
|
slug: "client-board",
|
|
description: "",
|
|
status: "active",
|
|
kind: "project",
|
|
color: "#0ea5e9",
|
|
repo_owner: "",
|
|
repo_name: "",
|
|
repo_url: "",
|
|
webhook_secret: "",
|
|
last_ingest_at: "",
|
|
session_gap_minutes: 15,
|
|
pre_session_minutes: 5,
|
|
post_session_minutes: 5,
|
|
background_md: "",
|
|
show_in_intake: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function card(overrides: Partial<CardRecord> = {}): CardRecord {
|
|
return {
|
|
id: "k1",
|
|
created: "2026-01-01 00:00:00.000Z",
|
|
updated: "2026-01-01 00:00:00.000Z",
|
|
project: "p1",
|
|
column: "col1",
|
|
title: "Refactor billing modal",
|
|
description: "",
|
|
position: 0,
|
|
priority: "medium",
|
|
target_date: "",
|
|
created_by: "",
|
|
created_by_role: "owner",
|
|
accepted: true,
|
|
start_date: "",
|
|
scope_rule: "",
|
|
archived: false,
|
|
number: 1,
|
|
narrative: "",
|
|
narrative_version: 0,
|
|
narrative_shas: "",
|
|
brief: null,
|
|
transcript: null,
|
|
size_band: "",
|
|
assignee: "",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function subtask(overrides: Partial<SubtaskRecord> = {}): SubtaskRecord {
|
|
return {
|
|
id: "s1",
|
|
created: "2026-01-01 00:00:00.000Z",
|
|
updated: "2026-01-01 00:00:00.000Z",
|
|
card: "k1",
|
|
title: "Write tests",
|
|
done: false,
|
|
position: 0,
|
|
start_date: "",
|
|
target_date: "",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function column(overrides: Partial<ColumnRecord> = {}): ColumnRecord {
|
|
return {
|
|
id: "col1",
|
|
created: "",
|
|
updated: "",
|
|
project: "p1",
|
|
name: "To Do",
|
|
position: 0,
|
|
is_inbox: false,
|
|
is_done: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function client(overrides: Partial<ClientRecord> = {}): ClientRecord {
|
|
return {
|
|
id: "c1",
|
|
created: "",
|
|
updated: "",
|
|
name: "Acme",
|
|
slug: "acme",
|
|
brand_color: "#0ea5e9",
|
|
active: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("buildPlanSections — groups cards by project, sorts by date", () => {
|
|
it("returns one section per active project, sorted client-then-project", () => {
|
|
const sections = buildPlanSections({
|
|
clients: [client({ id: "c2", name: "Bravo" }), client({ id: "c1", name: "Acme" })],
|
|
projects: [
|
|
project({ id: "p2", client: "c2", name: "Bravo Site", slug: "bravo-site" }),
|
|
project({ id: "p1", client: "c1", name: "Acme App", slug: "acme-app" }),
|
|
],
|
|
columns: [column()],
|
|
cards: [card({ project: "p2" }), card({ project: "p1" })],
|
|
subtasks: [],
|
|
});
|
|
|
|
expect(sections.map((s) => s.project.name)).toEqual(["Acme App", "Bravo Site"]);
|
|
});
|
|
|
|
it("excludes archived projects and Done column cards", () => {
|
|
const sections = buildPlanSections({
|
|
clients: [client()],
|
|
projects: [project({ status: "archived" })],
|
|
columns: [
|
|
column({ id: "col-done", name: "Done", is_done: true }),
|
|
column({ id: "col-todo", name: "To Do" }),
|
|
],
|
|
cards: [
|
|
card({ id: "k-archived-project", column: "col-todo" }),
|
|
card({ id: "k-done", column: "col-done" }),
|
|
],
|
|
subtasks: [],
|
|
});
|
|
|
|
expect(sections).toEqual([]);
|
|
});
|
|
|
|
it("sorts cards within a project: dated first by start_date asc, then undated by title", () => {
|
|
const sections = buildPlanSections({
|
|
clients: [client()],
|
|
projects: [project()],
|
|
columns: [column()],
|
|
cards: [
|
|
card({ id: "z-late", title: "Late", start_date: "2026-03-01", target_date: "2026-03-15" }),
|
|
card({ id: "a-early", title: "Early", start_date: "2026-02-01", target_date: "2026-02-10" }),
|
|
card({ id: "n-zeta", title: "Zeta" }),
|
|
card({ id: "n-alpha", title: "Alpha" }),
|
|
],
|
|
subtasks: [],
|
|
});
|
|
|
|
expect(sections[0].cards.map((c) => c.id)).toEqual([
|
|
"a-early",
|
|
"z-late",
|
|
"n-alpha",
|
|
"n-zeta",
|
|
]);
|
|
});
|
|
|
|
it("groups subtasks under their parent card and splits dated vs undated", () => {
|
|
const sections = buildPlanSections({
|
|
clients: [client()],
|
|
projects: [project()],
|
|
columns: [column()],
|
|
cards: [card({ id: "k1", start_date: "2026-02-01", target_date: "2026-02-10" })],
|
|
subtasks: [
|
|
subtask({ id: "s-dated", position: 1, start_date: "2026-02-01", target_date: "2026-02-05" }),
|
|
subtask({ id: "s-undated", position: 0 }),
|
|
],
|
|
});
|
|
|
|
const c = sections[0].cards[0];
|
|
expect(c.datedSubtasks.map((s) => s.id)).toEqual(["s-dated"]);
|
|
expect(c.undatedSubtaskCount).toBe(1);
|
|
});
|
|
|
|
it("sorts subtasks by position within a card", () => {
|
|
const sections = buildPlanSections({
|
|
clients: [client()],
|
|
projects: [project()],
|
|
columns: [column()],
|
|
cards: [card({ start_date: "2026-02-01", target_date: "2026-02-10" })],
|
|
subtasks: [
|
|
subtask({ id: "s2", position: 2, start_date: "2026-02-01", target_date: "2026-02-05" }),
|
|
subtask({ id: "s1", position: 1, start_date: "2026-02-01", target_date: "2026-02-03" }),
|
|
],
|
|
});
|
|
|
|
expect(sections[0].cards[0].datedSubtasks.map((s) => s.id)).toEqual(["s1", "s2"]);
|
|
});
|
|
});
|