pelagia-portal/App/app/(portal)/admin/vendors/actions.ts
Hardik a2c35d0a93 feat(admin): auto-generate structured IDs for users, vendors, accounts and cost centres
Users: employeeId auto-generated from role prefix (TCH/MAN/ACC/MGR/SUP/AUD/ADM)
followed by next sequential number; shown read-only in edit form, removed
from create form. Cost Centres: new code field (SITE-001 ...) added to
Vessel model with migration + backfill; auto-generated on create, read-only
in edit. Vendors and Accounts: code/vendorId inputs pre-filled with the
next suggested ID (VND-001, ACC-001) from the server page; user can override
with any PREFIX-NUMBER format, validated by regex.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 15:02:50 +05:30

196 lines
6.7 KiB
TypeScript

"use server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { hasPermission } from "@/lib/permissions";
import { geocodePincode } from "@/lib/geo";
import { z } from "zod";
import { revalidatePath } from "next/cache";
type ActionResult = { ok: true } | { error: string };
const contactSchema = z.object({
name: z.string().min(1),
role: z.string().optional(),
mobile: z.string().optional(),
email: z.string().optional(),
isPrimary: z.boolean().default(false),
});
const vendorSchema = z.object({
name: z.string().min(1, "Vendor name is required"),
vendorId: z.string().regex(/^[A-Z0-9]+-\d+$/i, "Vendor ID must be in format PREFIX-NUMBER (e.g. VND-001)").optional(),
address: z.string().optional(),
pincode: z.string().optional(),
gstin: z.string().optional(),
});
function parseContacts(formData: FormData) {
const contacts: z.infer<typeof contactSchema>[] = [];
let i = 0;
while (formData.has(`contacts[${i}].name`)) {
const name = (formData.get(`contacts[${i}].name`) as string).trim();
if (name) {
contacts.push({
name,
role: (formData.get(`contacts[${i}].role`) as string) || undefined,
mobile: (formData.get(`contacts[${i}].mobile`) as string) || undefined,
email: (formData.get(`contacts[${i}].email`) as string) || undefined,
isPrimary: formData.get(`contacts[${i}].isPrimary`) === "true",
});
}
i++;
}
return contacts;
}
async function resolveLatLng(pincode?: string) {
if (!pincode) return { latitude: null, longitude: null };
const coords = await geocodePincode(pincode);
return { latitude: coords?.lat ?? null, longitude: coords?.lng ?? null };
}
export async function createVendor(formData: FormData): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) {
return { error: "Unauthorized" };
}
const parsed = vendorSchema.safeParse({
name: formData.get("name"),
vendorId: formData.get("vendorId") || undefined,
address: formData.get("address") || undefined,
pincode: formData.get("pincode") || undefined,
gstin: formData.get("gstin") || undefined,
});
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
const data = parsed.data;
if (data.vendorId) {
const exists = await db.vendor.findUnique({ where: { vendorId: data.vendorId } });
if (exists) return { error: "A vendor with that Vendor ID already exists" };
}
const { latitude, longitude } = await resolveLatLng(data.pincode);
const contacts = parseContacts(formData);
await db.vendor.create({
data: {
name: data.name,
vendorId: data.vendorId ?? null,
address: data.address ?? null,
pincode: data.pincode ?? null,
gstin: data.gstin ?? null,
latitude,
longitude,
isVerified: !!data.vendorId,
contacts: contacts.length > 0 ? { create: contacts } : undefined,
},
});
revalidatePath("/admin/vendors");
return { ok: true };
}
export async function updateVendor(formData: FormData): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) {
return { error: "Unauthorized" };
}
const id = formData.get("id") as string;
if (!id) return { error: "Vendor ID is required" };
const parsed = vendorSchema.safeParse({
name: formData.get("name"),
vendorId: formData.get("vendorId") || undefined,
address: formData.get("address") || undefined,
pincode: formData.get("pincode") || undefined,
gstin: formData.get("gstin") || undefined,
});
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
const data = parsed.data;
if (data.vendorId) {
const conflict = await db.vendor.findFirst({ where: { vendorId: data.vendorId, id: { not: id } } });
if (conflict) return { error: "Another vendor already has that Vendor ID" };
}
const existing = await db.vendor.findUnique({ where: { id }, select: { pincode: true, latitude: true, longitude: true } });
const pincodeChanged = data.pincode !== (existing?.pincode ?? undefined);
const { latitude, longitude } = pincodeChanged
? await resolveLatLng(data.pincode)
: { latitude: existing?.latitude ?? null, longitude: existing?.longitude ?? null };
const contacts = parseContacts(formData);
await db.$transaction(async (tx) => {
await tx.vendor.update({
where: { id },
data: {
name: data.name,
vendorId: data.vendorId ?? null,
address: data.address ?? null,
pincode: data.pincode ?? null,
gstin: data.gstin ?? null,
latitude,
longitude,
isVerified: !!data.vendorId,
},
});
// Replace contacts wholesale
await tx.vendorContact.deleteMany({ where: { vendorId: id } });
if (contacts.length > 0) {
await tx.vendorContact.createMany({ data: contacts.map((c) => ({ ...c, vendorId: id })) });
}
});
revalidatePath("/admin/vendors");
revalidatePath(`/admin/vendors/${id}`);
return { ok: true };
}
export async function toggleVendorActive(vendorId: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) {
return { error: "Unauthorized" };
}
const vendor = await db.vendor.findUnique({ where: { id: vendorId }, select: { isActive: true } });
if (!vendor) return { error: "Vendor not found" };
await db.vendor.update({ where: { id: vendorId }, data: { isActive: !vendor.isActive } });
revalidatePath("/admin/vendors");
return { ok: true };
}
export async function deleteVendor(id: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) return { error: "Unauthorized" };
const blocked = await db.purchaseOrder.findFirst({
where: { vendorId: id, status: { not: "DRAFT" } },
});
if (blocked) return { error: "Cannot delete: vendor is referenced in submitted or active purchase orders." };
try {
await db.$transaction(
async (tx) => {
await tx.purchaseOrder.updateMany({ where: { vendorId: id, status: "DRAFT" }, data: { vendorId: null } });
await tx.product.updateMany({ where: { lastVendorId: id }, data: { lastVendorId: null } });
await tx.productVendorPrice.deleteMany({ where: { vendorId: id } });
await tx.vendor.delete({ where: { id } });
},
{ timeout: 30000 },
);
} catch (err: unknown) {
const code = (err as { code?: string })?.code;
if (code === "P2028") {
return { error: "Delete timed out — please try again." };
}
throw err;
}
revalidatePath("/admin/vendors");
return { ok: true };
}