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): 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(); 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(); 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((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 { const pb = await getServerPb(); if (!pb.authStore.isValid) return []; const [clients, projects, columns, cards, subtasks] = await Promise.all([ pb.collection("clients").getFullList(), pb.collection("projects").getFullList({ filter: 'status = "active"' }), pb.collection("columns").getFullList(), pb.collection("cards").getFullList({ filter: "archived = false" }), pb.collection("subtasks").getFullList({ fields: "card,done" }), ]); return buildPlanningBoard({ clients, projects, columns, cards, subtasks }); }