pelagia-portal/App/app/(portal)/admin/users/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

138 lines
4.5 KiB
TypeScript

"use server";
import { auth } from "@/auth";
import { db } from "@/lib/db";
import { hasPermission } from "@/lib/permissions";
import { z } from "zod";
import bcrypt from "bcryptjs";
import { revalidatePath } from "next/cache";
import type { Role } from "@prisma/client";
import { ROLE_PREFIX, nextId } from "@/lib/id-generators";
type ActionResult = { ok: true } | { error: string };
const userSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
role: z.enum(["TECHNICAL", "MANNING", "ACCOUNTS", "MANAGER", "SUPERUSER", "AUDITOR", "ADMIN"]),
password: z.string().min(8, "Password must be at least 8 characters").optional(),
});
export async function createUser(formData: FormData): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_users")) {
return { error: "Unauthorized" };
}
const parsed = userSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
role: formData.get("role"),
password: formData.get("password") || undefined,
});
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
const data = parsed.data;
if (!data.password) return { error: "Password is required for new users" };
const exists = await db.user.findFirst({
where: { email: data.email },
});
if (exists) return { error: "A user with that email already exists" };
// Auto-generate employeeId based on role prefix
const prefix = ROLE_PREFIX[data.role];
const existingIds = await db.user.findMany({
where: { role: data.role as Role },
select: { employeeId: true },
});
const employeeId = nextId(prefix, existingIds.map((u) => u.employeeId));
const passwordHash = await bcrypt.hash(data.password, 12);
await db.user.create({
data: {
employeeId,
name: data.name,
email: data.email,
role: data.role as Role,
passwordHash,
},
});
revalidatePath("/admin/users");
return { ok: true };
}
export async function updateUser(formData: FormData): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_users")) {
return { error: "Unauthorized" };
}
const id = formData.get("id") as string;
if (!id) return { error: "User ID is required" };
const parsed = userSchema.safeParse({
name: formData.get("name"),
email: formData.get("email"),
role: formData.get("role"),
password: formData.get("password") || undefined,
});
if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" };
const data = parsed.data;
const conflict = await db.user.findFirst({
where: {
AND: [
{ id: { not: id } },
{ email: data.email },
],
},
});
if (conflict) return { error: "Another user already has that email" };
const updateData: Parameters<typeof db.user.update>[0]["data"] = {
name: data.name,
email: data.email,
role: data.role as Role,
};
if (data.password) {
updateData.passwordHash = await bcrypt.hash(data.password, 12);
}
await db.user.update({ where: { id }, data: updateData });
revalidatePath("/admin/users");
return { ok: true };
}
export async function deleteUser(id: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_users")) return { error: "Unauthorized" };
if (id === session.user.id) return { error: "Cannot delete your own account." };
const inUse = await db.purchaseOrder.findFirst({ where: { submitterId: id } });
if (inUse) return { error: "Cannot delete: user has submitted purchase orders. Deactivate them instead." };
await db.$transaction(async (tx) => {
await tx.notification.deleteMany({ where: { userId: id } });
await tx.user.delete({ where: { id } });
});
revalidatePath("/admin/users");
return { ok: true };
}
export async function toggleUserActive(userId: string): Promise<ActionResult> {
const session = await auth();
if (!session?.user || !hasPermission(session.user.role, "manage_users")) {
return { error: "Unauthorized" };
}
if (userId === session.user.id) return { error: "You cannot deactivate your own account" };
const user = await db.user.findUnique({ where: { id: userId }, select: { isActive: true } });
if (!user) return { error: "User not found" };
await db.user.update({ where: { id: userId }, data: { isActive: !user.isActive } });
revalidatePath("/admin/users");
return { ok: true };
}