pelagia-portal/App/app/(portal)/admin/delivery-locations/actions.ts
Hardik 5aae45299b
All checks were successful
PR checks / checks (pull_request) Successful in 42s
PR checks / integration (pull_request) Successful in 30s
feat(po): admin-managed delivery locations + Place of Delivery dropdown (#19)
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>
2026-06-24 02:08:59 +05:30

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 };
}