kanban-dev-webapp/src/lib/planning/load.ts

130 lines
4.2 KiB
TypeScript

import "server-only";
import { getServerPb } from "@/lib/pb/server";
import { readProjectPlan, type PlanRange, type ProjectPlan } from "@/lib/planning/state";
import { sortPlanningCards } from "@/lib/planning/sort";
import type {
CardRecord,
ClientRecord,
ColumnRecord,
ProjectRecord,
SubtaskRecord,
} from "@/lib/pb/types";
export interface PlanningCard {
id: string;
created: string;
number: number;
title: string;
description: string;
priority: CardRecord["priority"];
columnName: string;
boardTargetDate: string;
draftRange: PlanRange | null;
approvedRange: PlanRange | null;
subtaskDone: number;
subtaskTotal: number;
href: string;
}
export interface PlanningProject {
id: string;
name: string;
clientName: string;
clientColor: string;
plan: ProjectPlan;
cards: PlanningCard[];
}
interface PlanningRows {
clients: ClientRecord[];
projects: ProjectRecord[];
columns: ColumnRecord[];
cards: CardRecord[];
subtasks: SubtaskRecord[];
}
/** Review and Done have left the planning queue. */
export function isPlannableColumn(column: Pick<ColumnRecord, "name" | "is_done">): boolean {
const name = column.name.trim().toLowerCase().replace(/\s+/g, " ");
return !column.is_done && name !== "review" && name !== "under review";
}
export function buildPlanningBoard({
clients,
projects,
columns,
cards,
subtasks,
}: PlanningRows): PlanningProject[] {
const clientsById = new Map(clients.map((client) => [client.id, client]));
const columnsById = new Map(columns.map((column) => [column.id, column]));
const counts = new Map<string, { done: number; total: number }>();
for (const subtask of subtasks) {
const count = counts.get(subtask.card) ?? { done: 0, total: 0 };
count.total += 1;
if (subtask.done) count.done += 1;
counts.set(subtask.card, count);
}
const cardsByProject = new Map<string, CardRecord[]>();
for (const card of cards) {
const column = columnsById.get(card.column);
if (card.archived || !column || !isPlannableColumn(column)) continue;
const list = cardsByProject.get(card.project) ?? [];
list.push(card);
cardsByProject.set(card.project, list);
}
const output: PlanningProject[] = [];
for (const project of projects) {
if (project.status !== "active") continue;
const client = clientsById.get(project.client);
if (!client) continue;
const plan = readProjectPlan(project.plan_data);
const projectCards = (cardsByProject.get(project.id) ?? []).map<PlanningCard>((card) => {
const count = counts.get(card.id) ?? { done: 0, total: 0 };
return {
id: card.id,
created: card.created,
number: card.number ?? 0,
title: card.title,
description: card.description ?? "",
priority: card.priority,
columnName: columnsById.get(card.column)?.name ?? "",
boardTargetDate: (card.target_date ?? "").slice(0, 10),
draftRange: plan.draft[card.id] ?? null,
approvedRange: plan.approved[card.id] ?? null,
subtaskDone: count.done,
subtaskTotal: count.total,
href: `/p/${client.slug}/${project.slug}/c/${card.id}`,
};
});
if (projectCards.length === 0) continue;
output.push({
id: project.id,
name: project.name,
clientName: client.name,
clientColor: client.brand_color || "var(--brand)",
plan,
cards: sortPlanningCards(projectCards, "date"),
});
}
return output.sort((a, b) =>
a.clientName.localeCompare(b.clientName) || a.name.localeCompare(b.name)
);
}
/** PocketBase read rules scope every collection to the signed-in client. */
export async function loadPlanningBoard(): Promise<PlanningProject[]> {
const pb = await getServerPb();
if (!pb.authStore.isValid) return [];
const [clients, projects, columns, cards, subtasks] = await Promise.all([
pb.collection("clients").getFullList<ClientRecord>(),
pb.collection("projects").getFullList<ProjectRecord>({ filter: 'status = "active"' }),
pb.collection("columns").getFullList<ColumnRecord>(),
pb.collection("cards").getFullList<CardRecord>({ filter: "archived = false" }),
pb.collection("subtasks").getFullList<SubtaskRecord>({ fields: "card,done" }),
]);
return buildPlanningBoard({ clients, projects, columns, cards, subtasks });
}