diff --git a/App/app/(portal)/admin/vendors/actions.ts b/App/app/(portal)/admin/vendors/actions.ts index f19d431..59da9fa 100644 --- a/App/app/(portal)/admin/vendors/actions.ts +++ b/App/app/(portal)/admin/vendors/actions.ts @@ -52,10 +52,14 @@ async function resolveLatLng(pincode?: string) { export async function createVendor(formData: FormData): Promise { const session = await auth(); - if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) { + if (!session?.user || !hasPermission(session.user.role, "create_vendor")) { return { error: "Unauthorized" }; } + // Submitters (no manage_vendors) may create vendors, but they stay UNVERIFIED + // until a PO closes with them or a Manager/Accounts/Admin approves them. + const canVerify = hasPermission(session.user.role, "manage_vendors"); + const parsed = vendorSchema.safeParse({ name: formData.get("name"), vendorId: formData.get("vendorId") || undefined, @@ -66,8 +70,10 @@ export async function createVendor(formData: FormData): Promise { 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 } }); + // Only verifiers may assign a Vendor ID (which marks a vendor as verified). + const vendorId = canVerify ? data.vendorId : undefined; + if (vendorId) { + const exists = await db.vendor.findUnique({ where: { vendorId } }); if (exists) return { error: "A vendor with that Vendor ID already exists" }; } @@ -77,18 +83,33 @@ export async function createVendor(formData: FormData): Promise { await db.vendor.create({ data: { name: data.name, - vendorId: data.vendorId ?? null, + vendorId: vendorId ?? null, address: data.address ?? null, pincode: data.pincode ?? null, gstin: data.gstin ?? null, latitude, longitude, - isVerified: !!data.vendorId, + isVerified: canVerify ? !!vendorId : false, contacts: contacts.length > 0 ? { create: contacts } : undefined, }, }); revalidatePath("/admin/vendors"); + revalidatePath("/inventory/vendors"); + return { ok: true }; +} + +/** Approve / verify a vendor — Manager, Accounts or Admin only. */ +export async function verifyVendor(vendorId: string): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_vendors")) { + return { error: "Unauthorized" }; + } + + await db.vendor.update({ where: { id: vendorId }, data: { isVerified: true } }); + revalidatePath("/admin/vendors"); + revalidatePath("/inventory/vendors"); + revalidatePath(`/admin/vendors/${vendorId}`); return { ok: true }; } diff --git a/App/app/(portal)/admin/vendors/vendor-form.tsx b/App/app/(portal)/admin/vendors/vendor-form.tsx index bed7474..7a4558d 100644 --- a/App/app/(portal)/admin/vendors/vendor-form.tsx +++ b/App/app/(portal)/admin/vendors/vendor-form.tsx @@ -113,7 +113,7 @@ function ContactsEditor({ initial }: { initial?: ContactRow[] }) { ); } -function VendorFormFields({ vendor, suggestedVendorId }: { vendor?: VendorRow; suggestedVendorId?: string }) { +function VendorFormFields({ vendor, suggestedVendorId, simple = false }: { vendor?: VendorRow; suggestedVendorId?: string; simple?: boolean }) { const [gstin, setGstin] = useState(vendor?.gstin ?? ""); const [name, setName] = useState(vendor?.name ?? ""); const [address, setAddress] = useState(vendor?.address ?? ""); @@ -217,11 +217,18 @@ function VendorFormFields({ vendor, suggestedVendorId }: { vendor?: VendorRow; s setName(e.target.value)} required className={INPUT} /> -
- - -
+ {!simple && ( +
+ + +
+ )} + {simple && ( +

+ This vendor will be created as unverified. It becomes verified once a PO is closed with it, or after a Manager/Accounts review. +

+ )} {/* Address + Pincode */}
@@ -243,7 +250,7 @@ function VendorFormFields({ vendor, suggestedVendorId }: { vendor?: VendorRow; s ); } -export function AddVendorButton({ suggestedId }: { suggestedId?: string }) { +export function AddVendorButton({ suggestedId, simple = false }: { suggestedId?: string; simple?: boolean }) { const router = useRouter(); const [open, setOpen] = useState(false); const [pending, setPending] = useState(false); @@ -264,7 +271,7 @@ export function AddVendorButton({ suggestedId }: { suggestedId?: string }) { setOpen(false)}>
- + {error &&

{error}

}
); diff --git a/App/app/(portal)/profile/page.tsx b/App/app/(portal)/profile/page.tsx index c82ee1d..81f1bea 100644 --- a/App/app/(portal)/profile/page.tsx +++ b/App/app/(portal)/profile/page.tsx @@ -1,6 +1,7 @@ import { auth } from "@/auth"; import { db } from "@/lib/db"; import { redirect } from "next/navigation"; +import { hasPermission } from "@/lib/permissions"; import { generateDownloadUrl } from "@/lib/storage"; import { ChangePasswordForm } from "./change-password-form"; import { SignatureUploader } from "./signature-uploader"; @@ -23,30 +24,44 @@ export default async function ProfilePage() { const session = await auth(); if (!session?.user) redirect("/login"); - const user = await db.user.findUnique({ - where: { id: session.user.id }, - select: { - id: true, - name: true, - email: true, - employeeId: true, - role: true, - signatureKey: true, - superUserRequests: { - orderBy: { createdAt: "desc" }, - take: 1, - select: { status: true, createdAt: true }, - }, + const userSelect = { + id: true, + name: true, + email: true, + employeeId: true, + role: true, + signatureKey: true, + passwordHash: true, + superUserRequests: { + orderBy: { createdAt: "desc" as const }, + take: 1, + select: { status: true, createdAt: true }, }, - }); + }; + + // Look up by id, falling back to email. SSO/no-password users can carry a JWT + // whose `id` differs from the DB row; the email fallback keeps the page reachable. + let user = await db.user.findUnique({ where: { id: session.user.id }, select: userSelect }); + if (!user && session.user.email) { + user = await db.user.findUnique({ where: { email: session.user.email }, select: userSelect }); + } if (!user) redirect("/login"); - const canHaveSignature = user.role === "MANAGER" || user.role === "SUPERUSER"; + // Only approvers (those who can approve POs) may upload a signature. + const canHaveSignature = hasPermission(user.role, "approve_po"); const canRequestSuperUser = user.role !== "SUPERUSER" && user.role !== "ADMIN"; + // SSO-only users have no password yet; the form lets them set one. + const hasPassword = !!user.passwordHash; - const signatureUrl = user.signatureKey - ? await generateDownloadUrl(user.signatureKey) - : null; + // Never let a storage hiccup (missing key, R2 misconfig) crash the profile page. + let signatureUrl: string | null = null; + if (user.signatureKey) { + try { + signatureUrl = await generateDownloadUrl(user.signatureKey); + } catch { + signatureUrl = null; + } + } const latestRequest = user.superUserRequests[0] ?? null; @@ -84,10 +99,17 @@ export default async function ProfilePage() { - {/* Change Password */} + {/* Change / Set Password */}
-

Change Password

- +

+ {hasPassword ? "Change Password" : "Set Password"} +

+ {!hasPassword && ( +

+ You sign in with single sign-on. Optionally set a password to also sign in with email. +

+ )} +
{/* Signature (managers & superusers) */} diff --git a/App/lib/permissions.ts b/App/lib/permissions.ts index 970ef12..320c08d 100644 --- a/App/lib/permissions.ts +++ b/App/lib/permissions.ts @@ -16,14 +16,15 @@ export type Permission = | "export_reports" | "manage_users" | "manage_vendors" + | "create_vendor" | "manage_vessels_accounts" | "manage_products" | "manage_sites"; const ROLE_PERMISSIONS: Record = { - TECHNICAL: ["create_po", "submit_po", "edit_own_draft_po", "view_own_pos", "confirm_receipt"], - MANNING: ["create_po", "submit_po", "edit_own_draft_po", "view_own_pos", "confirm_receipt"], - ACCOUNTS: ["view_all_pos", "process_payment", "manage_vendors"], + TECHNICAL: ["create_po", "submit_po", "edit_own_draft_po", "view_own_pos", "confirm_receipt", "create_vendor"], + MANNING: ["create_po", "submit_po", "edit_own_draft_po", "view_own_pos", "confirm_receipt", "create_vendor"], + ACCOUNTS: ["view_all_pos", "process_payment", "manage_vendors", "create_vendor"], MANAGER: [ "create_po", "submit_po", @@ -37,6 +38,7 @@ const ROLE_PERMISSIONS: Record = { "view_analytics", "export_reports", "manage_vendors", + "create_vendor", "manage_vessels_accounts", "manage_products", "manage_sites", @@ -55,6 +57,7 @@ const ROLE_PERMISSIONS: Record = { "confirm_receipt", "view_analytics", "export_reports", + "create_vendor", ], AUDITOR: ["view_own_pos", "view_all_pos", "view_analytics", "export_reports"], ADMIN: [ @@ -64,6 +67,7 @@ const ROLE_PERMISSIONS: Record = { "export_reports", "manage_users", "manage_vendors", + "create_vendor", "manage_vessels_accounts", "manage_products", "manage_sites",