From 53c4048ad5c4df8be9ca612992d9fc1b3224f17d Mon Sep 17 00:00:00 2001 From: Yashwin Date: Mon, 18 May 2026 16:55:18 +0530 Subject: [PATCH] refactor: enhance user validation schemas, improve code formatting, and debug storage statistics retrieval. --- src/components/shared/EditUserModal.tsx | 164 +++++++++++++----- src/components/shared/FileUploadModal.tsx | 67 +++++-- src/components/shared/NewUserModal.tsx | 4 +- .../dashboard/components/RecentActivity.tsx | 4 +- src/pages/superadmin/CreateTenantWizard.tsx | 59 ++++--- src/pages/superadmin/EditTenant.tsx | 10 +- src/pages/tenant/StorageDashboard.tsx | 2 + 7 files changed, 225 insertions(+), 85 deletions(-) diff --git a/src/components/shared/EditUserModal.tsx b/src/components/shared/EditUserModal.tsx index 69c0853..46d0667 100644 --- a/src/components/shared/EditUserModal.tsx +++ b/src/components/shared/EditUserModal.tsx @@ -23,19 +23,29 @@ import type { User } from "@/types/user"; const assignmentSchema = z.object({ role_id: z.string().min(1, "Role is required"), - module_ids: z.array(z.string().min(1)).min(1, "At least one module is required"), + module_ids: z + .array(z.string().min(1)) + .min(1, "At least one module is required"), }); // Validation schema const editUserSchema = z.object({ email: z.email({ message: "Please enter a valid email address" }), - first_name: z.string().min(1, "First name is required"), - last_name: z.string().min(1, "Last name is required"), + first_name: z + .string() + .min(1, "First name is required") + .max(255, "Maximum 255 characters allowed"), + last_name: z + .string() + .min(1, "Last name is required") + .max(255, "Maximum 255 characters allowed"), status: z.enum(["active", "suspended", "deleted"], { message: "Status is required", }), tenant_id: z.string().min(1, "Tenant is required"), - role_module_assignments: z.array(assignmentSchema).min(1, "At least one role assignment is required") + role_module_assignments: z + .array(assignmentSchema) + .min(1, "At least one role assignment is required") .superRefine((assignments, ctx) => { const seenModules = new Set(); assignments.forEach((assignment, rowIndex) => { @@ -239,20 +249,36 @@ export const EditUserModal = ({ loadedUserIdRef.current = userId; const tenantId = user.tenant?.id || user.tenant_id || ""; - + const roleOptions: { value: string; label: string }[] = []; const moduleOptions: { value: string; label: string }[] = []; - - let initialAssignments = [{ role_id: "", module_ids: [] as string[] }]; - - if (user.role_module_combinations && user.role_module_combinations.length > 0) { + + let initialAssignments = [ + { role_id: "", module_ids: [] as string[] }, + ]; + + if ( + user.role_module_combinations && + user.role_module_combinations.length > 0 + ) { const grouped = new Map(); user.role_module_combinations.forEach((c) => { - if (c.role_id && c.role_name && !roleOptions.some(o => o.value === c.role_id)) { + if ( + c.role_id && + c.role_name && + !roleOptions.some((o) => o.value === c.role_id) + ) { roleOptions.push({ value: c.role_id, label: c.role_name }); } - if (c.module_id && c.module_name && !moduleOptions.some(o => o.value === c.module_id)) { - moduleOptions.push({ value: c.module_id, label: c.module_name }); + if ( + c.module_id && + c.module_name && + !moduleOptions.some((o) => o.value === c.module_id) + ) { + moduleOptions.push({ + value: c.module_id, + label: c.module_name, + }); } if (c.module_id) { const existing = grouped.get(c.role_id) || []; @@ -260,23 +286,37 @@ export const EditUserModal = ({ grouped.set(c.role_id, existing); } }); - initialAssignments = Array.from(grouped.entries()).map(([role_id, module_ids]) => ({ - role_id, - module_ids, - })); + initialAssignments = Array.from(grouped.entries()).map( + ([role_id, module_ids]) => ({ + role_id, + module_ids, + }), + ); } else { // Fallback for older format - const r_ids = user.roles?.map(r => r.id) || (user.role_id ? [user.role_id] : []); + const r_ids = + user.roles?.map((r) => r.id) || + (user.role_id ? [user.role_id] : []); if (r_ids.length > 0) { - initialAssignments = r_ids.map(id => ({ role_id: id, module_ids: [] })); + initialAssignments = r_ids.map((id) => ({ + role_id: id, + module_ids: [], + })); } if (user.roles?.length) { - user.roles.forEach(r => roleOptions.push({ value: r.id, label: r.name })); + user.roles.forEach((r) => + roleOptions.push({ value: r.id, label: r.name }), + ); } else if (user.role?.id) { - roleOptions.push({ value: user.role.id, label: user.role.name }); + roleOptions.push({ + value: user.role.id, + label: user.role.name, + }); } if (user.modules?.length) { - user.modules.forEach(m => moduleOptions.push({ value: m.id, label: m.name })); + user.modules.forEach((m) => + moduleOptions.push({ value: m.id, label: m.name }), + ); } } @@ -406,10 +446,13 @@ export const EditUserModal = ({ } else if (!data.supplier_id) { data.supplier_id = null; } - + const { role_module_assignments, ...submitData } = data; const role_module_combinations = role_module_assignments.flatMap((row) => - row.module_ids.map((module_id) => ({ role_id: row.role_id, module_id })), + row.module_ids.map((module_id) => ({ + role_id: row.role_id, + module_id, + })), ); await onSubmit(userId, { ...submitData, role_module_combinations }); } catch (error: any) { @@ -556,7 +599,9 @@ export const EditUserModal = ({ required placeholder="Select Department" value={departmentIdValue || ""} - onValueChange={(value) => setValue("department_id", value, { shouldValidate: true })} + onValueChange={(value) => + setValue("department_id", value, { shouldValidate: true }) + } onLoadOptions={loadDepartments} initialOption={initialDepartmentOption || undefined} error={errors.department_id?.message} @@ -566,7 +611,9 @@ export const EditUserModal = ({ required placeholder="Select Designation" value={designationIdValue || ""} - onValueChange={(value) => setValue("designation_id", value, { shouldValidate: true })} + onValueChange={(value) => + setValue("designation_id", value, { shouldValidate: true }) + } onLoadOptions={loadDesignations} initialOption={initialDesignationOption || undefined} error={errors.designation_id?.message} @@ -627,7 +674,9 @@ export const EditUserModal = ({
- +
- + {(errors.role_module_assignments as any)?.message && (

{(errors.role_module_assignments as any).message} @@ -645,41 +694,76 @@ export const EditUserModal = ({

{fields.map((field, index) => { - const roleIdValue = watch(`role_module_assignments.${index}.role_id`); - const moduleIdsValue = watch(`role_module_assignments.${index}.module_ids`) || []; - + const roleIdValue = watch( + `role_module_assignments.${index}.role_id`, + ); + const moduleIdsValue = + watch(`role_module_assignments.${index}.module_ids`) || []; + // Extract specific label if available from initial options const getRoleLabel = (val: string) => { - const opt = initialRoleOptions.find(o => o.value === val); + const opt = initialRoleOptions.find((o) => o.value === val); return opt ? opt.label : undefined; }; const initialSelectedModules = moduleIdsValue .map((val) => { - const opt = initialModuleOptions.find((o) => o.value === val); - return opt ? { value: opt.value, label: opt.label } : null; + const opt = initialModuleOptions.find( + (o) => o.value === val, + ); + return opt + ? { value: opt.value, label: opt.label } + : null; }) - .filter((opt): opt is { value: string; label: string } => Boolean(opt)); - + .filter((opt): opt is { value: string; label: string } => + Boolean(opt), + ); + return ( -
+
setValue(`role_module_assignments.${index}.role_id`, value, { shouldValidate: true })} + onValueChange={(value) => + setValue( + `role_module_assignments.${index}.role_id`, + value, + { shouldValidate: true }, + ) + } onLoadOptions={loadRoles} - initialOption={roleIdValue ? { value: roleIdValue, label: getRoleLabel(roleIdValue) || roleIdValue } : undefined} - error={errors.role_module_assignments?.[index]?.role_id?.message} + initialOption={ + roleIdValue + ? { + value: roleIdValue, + label: + getRoleLabel(roleIdValue) || roleIdValue, + } + : undefined + } + error={ + errors.role_module_assignments?.[index]?.role_id + ?.message + } /> setValue(`role_module_assignments.${index}.module_ids`, value, { shouldValidate: true })} + onValueChange={(value) => + setValue( + `role_module_assignments.${index}.module_ids`, + value, + { shouldValidate: true }, + ) + } onLoadOptions={loadModules} initialOptions={initialSelectedModules} error={getModuleError(index)} diff --git a/src/components/shared/FileUploadModal.tsx b/src/components/shared/FileUploadModal.tsx index 9842d2a..dd578ca 100644 --- a/src/components/shared/FileUploadModal.tsx +++ b/src/components/shared/FileUploadModal.tsx @@ -43,6 +43,7 @@ import { z } from "zod"; import { Input } from "@/components/ui/input"; import fileAttachmentService, { type CategoriesFilterOptions, + type StorageQuota, } from "@/services/file-attachment-service"; // ───────────────────────────────────────────────────────────────────────────── @@ -61,9 +62,9 @@ function getExt(name: string) { return name.slice(((name.lastIndexOf(".") - 1) >>> 0) + 1).toLowerCase(); } -function isBlocked(name: string) { - return BLOCKED_EXTENSIONS.includes(getExt(name) ? `.${getExt(name)}` : ""); -} +// function isBlocked(name: string) { +// return BLOCKED_EXTENSIONS.includes(getExt(name) ? `.${getExt(name)}` : ""); +// } function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; @@ -204,6 +205,23 @@ export const FileUploadModal = ({ // File entries with upload progress (internal state kept separate from zod files array for progress tracking) const [fileEntries, setFileEntries] = useState([]); + + // Storage quota settings fetched dynamically from backend + const [quota, setQuota] = useState(null); + + useEffect(() => { + if (isOpen) { + fileAttachmentService.getQuota() + .then((res) => { + if (res?.data) { + setQuota(res.data); + } + }) + .catch((err) => { + console.error("Failed to load quota settings in FileUploadModal:", err); + }); + } + }, [isOpen]); // ── Auto-generate Entity ID ── useEffect(() => { @@ -242,15 +260,38 @@ export const FileUploadModal = ({ // ── Add files ── const addFiles = useCallback((incoming: File[]) => { + const activeBlocked = quota?.blocked_extensions || BLOCKED_EXTENSIONS; + const activeMaxSizeBytes = quota?.max_file_size_bytes || (MAX_SIZE_MB * 1024 * 1024); + const newEntries: FileEntry[] = incoming .slice(0, MAX_FILES) - .map((file) => ({ - file, - id: `${file.name}-${file.size}-${Date.now()}-${Math.random()}`, - status: isBlocked(file.name) ? "blocked" : "idle", - progress: 0, - error: isBlocked(file.name) ? "Blocked file type" : undefined, - })); + .map((file) => { + const ext = getExt(file.name); + const fileExt = ext ? `.${ext}` : ""; + const isBlockedType = activeBlocked.some( + (blockedExt) => blockedExt.toLowerCase() === fileExt.toLowerCase() + ); + const isTooLarge = file.size > activeMaxSizeBytes; + + let status: FileStatus = "idle"; + let error: string | undefined; + + if (isBlockedType) { + status = "blocked"; + error = "Blocked file type"; + } else if (isTooLarge) { + status = "blocked"; + error = `Exceeds limit of ${formatBytes(activeMaxSizeBytes)}`; + } + + return { + file, + id: `${file.name}-${file.size}-${Date.now()}-${Math.random()}`, + status, + progress: 0, + error, + }; + }); setFileEntries((prev) => { const combined = [...prev, ...newEntries]; @@ -259,7 +300,7 @@ export const FileUploadModal = ({ setValue("files", limited.map(e => e.file), { shouldValidate: true }); return limited; }); - }, [setValue]); + }, [setValue, quota]); const onDrop = useCallback( (e: DragEvent) => { @@ -433,7 +474,9 @@ export const FileUploadModal = ({

Attach supporting source files via File Attachment Service

-

PDF, DOCX, XLSX up to {MAX_SIZE_MB}MB

+

+ Allowed formats up to {quota ? formatBytes(quota.max_file_size_bytes) : `${MAX_SIZE_MB}MB`} +

) : ( diff --git a/src/components/shared/NewUserModal.tsx b/src/components/shared/NewUserModal.tsx index 7133342..72681f8 100644 --- a/src/components/shared/NewUserModal.tsx +++ b/src/components/shared/NewUserModal.tsx @@ -28,8 +28,8 @@ const assignmentSchema = z.object({ const newUserSchema = z .object({ email: z.email({ message: "Please enter a valid email address" }), - first_name: z.string().min(1, "First name is required"), - last_name: z.string().min(1, "Last name is required"), + first_name: z.string().min(1, "First name is required").max(255, "Maximum 255 characters allowed"), + last_name: z.string().min(1, "Last name is required").max(255, "Maximum 255 characters allowed"), status: z.enum(["active", "suspended", "deleted"], { message: "Status is required" }), auth_provider: z.enum(["local"], { message: "Auth provider is required" }), role_module_assignments: z.array(assignmentSchema).min(1, "At least one role assignment is required") diff --git a/src/features/dashboard/components/RecentActivity.tsx b/src/features/dashboard/components/RecentActivity.tsx index 8b21602..e554da8 100644 --- a/src/features/dashboard/components/RecentActivity.tsx +++ b/src/features/dashboard/components/RecentActivity.tsx @@ -145,7 +145,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {