Replaces the free-text "Place of Delivery" with a dropdown sourced from a new admin-managed Delivery Locations list (each = a Company FK + free-text address). - schema + migration: new DeliveryLocation model (companyId, address, isActive). - permission: manage_delivery_locations granted to Manager + SuperUser + Admin (Manager-accessible, not admin-only, per the issue). - admin screen /admin/delivery-locations: table + Add/Edit dialogs + activate/deactivate + delete (mirrors /admin/sites); sidebar link under Administration for Manager/SuperUser/Admin. - PO forms (new / edit / manager-edit): shared <DeliveryLocationField> native select populated from active locations, formatted "Company — address". - PurchaseOrder.placeOfDelivery stays a free-text SNAPSHOT (no FK) — the dropdown only changes how the value is picked, so export/import/historical POs are unchanged, and an edit preserves a current value not in the list as a "(current)" option. Deleting a location is therefore always safe. - tests: delivery-location CRUD + permission guard (6). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
2.7 KiB
TypeScript
77 lines
2.7 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";
|
|
|
|
const schema = z.object({
|
|
companyId: z.string().min(1, "Company is required"),
|
|
address: z.string().trim().min(1, "Delivery address 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_delivery_locations")) {
|
|
return { error: "Forbidden" };
|
|
}
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function createDeliveryLocation(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 };
|
|
|
|
// Guard against a dangling FK if the company was removed concurrently.
|
|
const company = await db.company.findUnique({ where: { id: parsed.data.companyId }, select: { id: true } });
|
|
if (!company) return { error: "Selected company no longer exists." };
|
|
|
|
await db.deliveryLocation.create({
|
|
data: { companyId: parsed.data.companyId, address: parsed.data.address },
|
|
});
|
|
revalidatePath("/admin/delivery-locations");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function updateDeliveryLocation(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.deliveryLocation.update({
|
|
where: { id },
|
|
data: { companyId: parsed.data.companyId, address: parsed.data.address },
|
|
});
|
|
revalidatePath("/admin/delivery-locations");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function toggleDeliveryLocationActive(id: string): Promise<Result> {
|
|
const g = await guard();
|
|
if ("error" in g) return g;
|
|
|
|
const loc = await db.deliveryLocation.findUnique({ where: { id }, select: { isActive: true } });
|
|
if (!loc) return { error: "Not found" };
|
|
await db.deliveryLocation.update({ where: { id }, data: { isActive: !loc.isActive } });
|
|
revalidatePath("/admin/delivery-locations");
|
|
return { ok: true };
|
|
}
|
|
|
|
export async function deleteDeliveryLocation(id: string): Promise<Result> {
|
|
const g = await guard();
|
|
if ("error" in g) return g;
|
|
|
|
// Safe to delete: POs keep their place-of-delivery as a text snapshot, so no
|
|
// purchase order references this row.
|
|
await db.deliveryLocation.delete({ where: { id } });
|
|
revalidatePath("/admin/delivery-locations");
|
|
return { ok: true };
|
|
}
|