Mirrors the Place-of-Delivery (#19) pattern: an admin clause library that feeds the PO T&C fields as dropdowns. (No "work order" type — POs only, per steer.) - schema + migration: TermsCondition (category enum + text + isActive); the migration seeds the prior TC_DEFAULTS as the starting clauses. - permission manage_terms (Manager + SuperUser + Admin). - admin screen /admin/terms: table + Add/Edit dialogs + activate/deactivate + delete (mirrors /admin/delivery-locations); sidebar link under Administration. - PO forms (new / edit / manager-edit): the five named T&C slots (Delivery / Dispatch / Inspection / Transit Insurance / Payment Terms) become a shared <TermsField> select sourced from active clauses of that category; "Others" stays free text; the fixed boilerplate lines are untouched. - tc* columns stay free-text SNAPSHOTS (export/import unchanged); a current value not among active clauses is preserved as a "(current)" option. - tests: terms CRUD + permission guard + grouping helper (6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
68 lines
2.3 KiB
TypeScript
68 lines
2.3 KiB
TypeScript
"use server";
|
|
|
|
import { auth } from "@/auth";
|
|
import { db } from "@/lib/db";
|
|
import { hasPermission } from "@/lib/permissions";
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
import { TermsCategory } from "@prisma/client";
|
|
|
|
const schema = z.object({
|
|
category: z.nativeEnum(TermsCategory),
|
|
text: z.string().trim().min(1, "Clause text is required"),
|
|
});
|
|
|
|
type Result = { ok: true } | { error: string };
|
|
|
|
async function guard(): Promise<{ ok: true } | { error: string }> {
|
|
const session = await auth();
|
|
if (!session?.user || !hasPermission(session.user.role, "manage_terms")) {
|
|
return { error: "Forbidden" };
|
|
}
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function createTerm(formData: FormData): Promise<Result> {
|
|
const g = await guard();
|
|
if ("error" in g) return g;
|
|
|
|
const parsed = schema.safeParse(Object.fromEntries(formData));
|
|
if (!parsed.success) return { error: parsed.error.errors[0].message };
|
|
|
|
await db.termsCondition.create({ data: { category: parsed.data.category, text: parsed.data.text } });
|
|
revalidatePath("/admin/terms");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function updateTerm(id: string, formData: FormData): Promise<Result> {
|
|
const g = await guard();
|
|
if ("error" in g) return g;
|
|
|
|
const parsed = schema.safeParse(Object.fromEntries(formData));
|
|
if (!parsed.success) return { error: parsed.error.errors[0].message };
|
|
|
|
await db.termsCondition.update({ where: { id }, data: { category: parsed.data.category, text: parsed.data.text } });
|
|
revalidatePath("/admin/terms");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function toggleTermActive(id: string): Promise<Result> {
|
|
const g = await guard();
|
|
if ("error" in g) return g;
|
|
|
|
const term = await db.termsCondition.findUnique({ where: { id }, select: { isActive: true } });
|
|
if (!term) return { error: "Not found" };
|
|
await db.termsCondition.update({ where: { id }, data: { isActive: !term.isActive } });
|
|
revalidatePath("/admin/terms");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function deleteTerm(id: string): Promise<Result> {
|
|
const g = await guard();
|
|
if ("error" in g) return g;
|
|
|
|
// Safe to delete: POs keep their T&C as text snapshots, so no PO references this row.
|
|
await db.termsCondition.delete({ where: { id } });
|
|
revalidatePath("/admin/terms");
|
|
return { ok: true };
|
|
}
|