132 lines
4.4 KiB
TypeScript
132 lines
4.4 KiB
TypeScript
import "server-only";
|
|
|
|
import { getServerPb } from "@/lib/pb/server";
|
|
import { readProjectPlan, type PlanRange, type ProjectPlan } from "@/lib/planning/state";
|
|
import type {
|
|
CardRecord,
|
|
ClientRecord,
|
|
ColumnRecord,
|
|
ProjectRecord,
|
|
SubtaskRecord,
|
|
} from "@/lib/pb/types";
|
|
|
|
export interface PlanningCard {
|
|
id: string;
|
|
number: number;
|
|
title: 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,
|
|
number: card.number ?? 0,
|
|
title: card.title,
|
|
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;
|
|
projectCards.sort((a, b) => {
|
|
const aDate = a.draftRange?.start ?? a.approvedRange?.start ?? "";
|
|
const bDate = b.draftRange?.start ?? b.approvedRange?.start ?? "";
|
|
if (!aDate && bDate) return -1;
|
|
if (aDate && !bDate) return 1;
|
|
return aDate.localeCompare(bDate) || a.title.localeCompare(b.title);
|
|
});
|
|
output.push({
|
|
id: project.id,
|
|
name: project.name,
|
|
clientName: client.name,
|
|
clientColor: client.brand_color || "var(--brand)",
|
|
plan,
|
|
cards: projectCards,
|
|
});
|
|
}
|
|
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 });
|
|
}
|