- number field on cards, unique per project - backfilled existing cards in created order - both create paths (owner action, client API) assign on creation - seed script is idempotent across re-runs
129 lines
No EOL
3.9 KiB
TypeScript
129 lines
No EOL
3.9 KiB
TypeScript
import { getAdminPb } from "@/lib/pb/admin";
|
|
import { requireSession } from "@/lib/auth/session";
|
|
import {
|
|
AccessError,
|
|
assertClientOwnsProject,
|
|
canWriteClientFields,
|
|
} from "@/lib/auth/access";
|
|
import { pickClientCardCreate } from "@/lib/cards/client-patch";
|
|
import { positionBetween } from "@/lib/ordering/fractional";
|
|
import { jsonError, jsonOk } from "@/lib/http/json";
|
|
import type { CardRecord, ColumnRecord, ProjectRecord } from "@/lib/pb/types";
|
|
|
|
/**
|
|
* POST /api/client/cards — a client manager proposes a new card.
|
|
*
|
|
* The destination column, priority and accepted flag are chosen here, never
|
|
* taken from the request. A card always lands in the project's Inbox at
|
|
* medium priority with accepted=false, whatever the body asks for. The owner
|
|
* accepts it by dragging it out, which is what sets accepted=true.
|
|
*/
|
|
export async function POST(request: Request): Promise<Response> {
|
|
try {
|
|
const session = await requireSession();
|
|
|
|
let body: unknown;
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
return jsonError("Invalid request body", 400);
|
|
}
|
|
|
|
const projectId =
|
|
typeof body === "object" && body !== null
|
|
? (body as Record<string, unknown>).project
|
|
: undefined;
|
|
|
|
if (typeof projectId !== "string" || !projectId) {
|
|
return jsonError("A project id is required", 400);
|
|
}
|
|
|
|
const pb = await getAdminPb();
|
|
|
|
let project: ProjectRecord;
|
|
try {
|
|
project = await pb
|
|
.collection("projects")
|
|
.getOne<ProjectRecord>(projectId);
|
|
} catch {
|
|
return jsonError("Project not found", 404);
|
|
}
|
|
|
|
assertClientOwnsProject(session, project);
|
|
|
|
if (!canWriteClientFields(session, project.client)) {
|
|
return jsonError("Project not found", 404);
|
|
}
|
|
|
|
const { input, dropped } = pickClientCardCreate(body);
|
|
|
|
if (dropped.length > 0) {
|
|
console.warn(
|
|
`[client-create] user=${session.userId} project=${projectId} dropped=${dropped.join(",")}`
|
|
);
|
|
}
|
|
|
|
if (!input) {
|
|
return jsonError("A title is required", 400);
|
|
}
|
|
|
|
const inbox = await pb
|
|
.collection("columns")
|
|
.getFirstListItem<ColumnRecord>(
|
|
`project = "${projectId}" && is_inbox = true`
|
|
)
|
|
.catch(() => null);
|
|
|
|
if (!inbox) {
|
|
// The column invariants in src/lib/columns/seed.ts should make this
|
|
// impossible. If it happens, the project's columns are corrupt and
|
|
// silently inventing a destination would hide that.
|
|
console.error(`[client-create] project ${projectId} has no inbox column`);
|
|
return jsonError("This project cannot accept new cards", 409);
|
|
}
|
|
|
|
const last = await pb
|
|
.collection("cards")
|
|
.getFirstListItem<CardRecord>(`column = "${inbox.id}"`, {
|
|
sort: "-position",
|
|
})
|
|
.catch(() => null);
|
|
|
|
const numbered = await pb.collection("cards").getFullList<CardRecord>({
|
|
filter: `project = "${projectId}"`,
|
|
fields: "number",
|
|
});
|
|
|
|
const card = await pb.collection("cards").create<CardRecord>({
|
|
project: projectId,
|
|
column: inbox.id,
|
|
title: input.title,
|
|
description: input.description,
|
|
position: positionBetween(last ? last.position : null, null),
|
|
priority: "medium",
|
|
created_by: session.userId,
|
|
created_by_role: "client",
|
|
accepted: false,
|
|
archived: false,
|
|
number: nextCardNumber(numbered.map((c) => c.number)),
|
|
});
|
|
|
|
await pb.collection("activity").create({
|
|
project: projectId,
|
|
card: card.id,
|
|
actor: session.userId,
|
|
action: "card_created",
|
|
field: "",
|
|
old_value: "",
|
|
new_value: card.title,
|
|
});
|
|
|
|
return jsonOk({ card: { id: card.id, title: card.title } }, 201);
|
|
} catch (error) {
|
|
if (error instanceof AccessError) {
|
|
return jsonError(error.message, error.status);
|
|
}
|
|
console.error("[client-create] unexpected error", error);
|
|
return jsonError("Something went wrong", 500);
|
|
}
|
|
} |