refactor: enhance user validation schemas, improve code formatting, and debug storage statistics retrieval.

This commit is contained in:
Yashwin 2026-05-18 16:55:18 +05:30
parent 793fa23c1b
commit 53c4048ad5
7 changed files with 225 additions and 85 deletions

View File

@ -23,19 +23,29 @@ import type { User } from "@/types/user";
const assignmentSchema = z.object({ const assignmentSchema = z.object({
role_id: z.string().min(1, "Role is required"), 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 // Validation schema
const editUserSchema = z.object({ const editUserSchema = z.object({
email: z.email({ message: "Please enter a valid email address" }), email: z.email({ message: "Please enter a valid email address" }),
first_name: z.string().min(1, "First name is required"), first_name: z
last_name: z.string().min(1, "Last name is required"), .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"], { status: z.enum(["active", "suspended", "deleted"], {
message: "Status is required", message: "Status is required",
}), }),
tenant_id: z.string().min(1, "Tenant 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) => { .superRefine((assignments, ctx) => {
const seenModules = new Set<string>(); const seenModules = new Set<string>();
assignments.forEach((assignment, rowIndex) => { assignments.forEach((assignment, rowIndex) => {
@ -243,16 +253,32 @@ export const EditUserModal = ({
const roleOptions: { value: string; label: string }[] = []; const roleOptions: { value: string; label: string }[] = [];
const moduleOptions: { value: string; label: string }[] = []; const moduleOptions: { value: string; label: string }[] = [];
let initialAssignments = [{ role_id: "", module_ids: [] as string[] }]; let initialAssignments = [
{ role_id: "", module_ids: [] as string[] },
];
if (user.role_module_combinations && user.role_module_combinations.length > 0) { if (
user.role_module_combinations &&
user.role_module_combinations.length > 0
) {
const grouped = new Map<string, string[]>(); const grouped = new Map<string, string[]>();
user.role_module_combinations.forEach((c) => { 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 }); 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)) { if (
moduleOptions.push({ value: c.module_id, label: c.module_name }); 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) { if (c.module_id) {
const existing = grouped.get(c.role_id) || []; const existing = grouped.get(c.role_id) || [];
@ -260,23 +286,37 @@ export const EditUserModal = ({
grouped.set(c.role_id, existing); grouped.set(c.role_id, existing);
} }
}); });
initialAssignments = Array.from(grouped.entries()).map(([role_id, module_ids]) => ({ initialAssignments = Array.from(grouped.entries()).map(
([role_id, module_ids]) => ({
role_id, role_id,
module_ids, module_ids,
})); }),
);
} else { } else {
// Fallback for older format // 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) { 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) { 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) { } 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) { 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 }),
);
} }
} }
@ -409,7 +449,10 @@ export const EditUserModal = ({
const { role_module_assignments, ...submitData } = data; const { role_module_assignments, ...submitData } = data;
const role_module_combinations = role_module_assignments.flatMap((row) => 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 }); await onSubmit(userId, { ...submitData, role_module_combinations });
} catch (error: any) { } catch (error: any) {
@ -556,7 +599,9 @@ export const EditUserModal = ({
required required
placeholder="Select Department" placeholder="Select Department"
value={departmentIdValue || ""} value={departmentIdValue || ""}
onValueChange={(value) => setValue("department_id", value, { shouldValidate: true })} onValueChange={(value) =>
setValue("department_id", value, { shouldValidate: true })
}
onLoadOptions={loadDepartments} onLoadOptions={loadDepartments}
initialOption={initialDepartmentOption || undefined} initialOption={initialDepartmentOption || undefined}
error={errors.department_id?.message} error={errors.department_id?.message}
@ -566,7 +611,9 @@ export const EditUserModal = ({
required required
placeholder="Select Designation" placeholder="Select Designation"
value={designationIdValue || ""} value={designationIdValue || ""}
onValueChange={(value) => setValue("designation_id", value, { shouldValidate: true })} onValueChange={(value) =>
setValue("designation_id", value, { shouldValidate: true })
}
onLoadOptions={loadDesignations} onLoadOptions={loadDesignations}
initialOption={initialDesignationOption || undefined} initialOption={initialDesignationOption || undefined}
error={errors.designation_id?.message} error={errors.designation_id?.message}
@ -627,7 +674,9 @@ export const EditUserModal = ({
<div className="mt-2 mb-4"> <div className="mt-2 mb-4">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-gray-700">Role & Module Assignments</label> <label className="text-sm font-medium text-gray-700">
Role & Module Assignments
</label>
<button <button
type="button" type="button"
onClick={() => append({ role_id: "", module_ids: [] })} onClick={() => append({ role_id: "", module_ids: [] })}
@ -645,41 +694,76 @@ export const EditUserModal = ({
<div className="space-y-3"> <div className="space-y-3">
{fields.map((field, index) => { {fields.map((field, index) => {
const roleIdValue = watch(`role_module_assignments.${index}.role_id`); const roleIdValue = watch(
const moduleIdsValue = watch(`role_module_assignments.${index}.module_ids`) || []; `role_module_assignments.${index}.role_id`,
);
const moduleIdsValue =
watch(`role_module_assignments.${index}.module_ids`) || [];
// Extract specific label if available from initial options // Extract specific label if available from initial options
const getRoleLabel = (val: string) => { 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; return opt ? opt.label : undefined;
}; };
const initialSelectedModules = moduleIdsValue const initialSelectedModules = moduleIdsValue
.map((val) => { .map((val) => {
const opt = initialModuleOptions.find((o) => o.value === val); const opt = initialModuleOptions.find(
return opt ? { value: opt.value, label: opt.label } : null; (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 ( return (
<div key={field.id} className="flex gap-2 items-start border p-3 rounded-md bg-gray-50 relative"> <div
key={field.id}
className="flex gap-2 items-start border p-3 rounded-md bg-gray-50 relative"
>
<div className="flex-1 grid grid-cols-2 gap-3"> <div className="flex-1 grid grid-cols-2 gap-3">
<PaginatedSelect <PaginatedSelect
label="Select Role" label="Select Role"
required required
placeholder="Select Role" placeholder="Select Role"
value={roleIdValue || ""} value={roleIdValue || ""}
onValueChange={(value) => setValue(`role_module_assignments.${index}.role_id`, value, { shouldValidate: true })} onValueChange={(value) =>
setValue(
`role_module_assignments.${index}.role_id`,
value,
{ shouldValidate: true },
)
}
onLoadOptions={loadRoles} onLoadOptions={loadRoles}
initialOption={roleIdValue ? { value: roleIdValue, label: getRoleLabel(roleIdValue) || roleIdValue } : undefined} initialOption={
error={errors.role_module_assignments?.[index]?.role_id?.message} roleIdValue
? {
value: roleIdValue,
label:
getRoleLabel(roleIdValue) || roleIdValue,
}
: undefined
}
error={
errors.role_module_assignments?.[index]?.role_id
?.message
}
/> />
<MultiselectPaginatedSelect <MultiselectPaginatedSelect
label="Select Modules" label="Select Modules"
required required
placeholder="Select Modules" placeholder="Select Modules"
value={moduleIdsValue} value={moduleIdsValue}
onValueChange={(value) => setValue(`role_module_assignments.${index}.module_ids`, value, { shouldValidate: true })} onValueChange={(value) =>
setValue(
`role_module_assignments.${index}.module_ids`,
value,
{ shouldValidate: true },
)
}
onLoadOptions={loadModules} onLoadOptions={loadModules}
initialOptions={initialSelectedModules} initialOptions={initialSelectedModules}
error={getModuleError(index)} error={getModuleError(index)}

View File

@ -43,6 +43,7 @@ import { z } from "zod";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import fileAttachmentService, { import fileAttachmentService, {
type CategoriesFilterOptions, type CategoriesFilterOptions,
type StorageQuota,
} from "@/services/file-attachment-service"; } from "@/services/file-attachment-service";
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
@ -61,9 +62,9 @@ function getExt(name: string) {
return name.slice(((name.lastIndexOf(".") - 1) >>> 0) + 1).toLowerCase(); return name.slice(((name.lastIndexOf(".") - 1) >>> 0) + 1).toLowerCase();
} }
function isBlocked(name: string) { // function isBlocked(name: string) {
return BLOCKED_EXTENSIONS.includes(getExt(name) ? `.${getExt(name)}` : ""); // return BLOCKED_EXTENSIONS.includes(getExt(name) ? `.${getExt(name)}` : "");
} // }
function formatBytes(bytes: number): string { function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B"; if (bytes === 0) return "0 B";
@ -205,6 +206,23 @@ export const FileUploadModal = ({
// File entries with upload progress (internal state kept separate from zod files array for progress tracking) // File entries with upload progress (internal state kept separate from zod files array for progress tracking)
const [fileEntries, setFileEntries] = useState<FileEntry[]>([]); const [fileEntries, setFileEntries] = useState<FileEntry[]>([]);
// Storage quota settings fetched dynamically from backend
const [quota, setQuota] = useState<StorageQuota | null>(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 ── // ── Auto-generate Entity ID ──
useEffect(() => { useEffect(() => {
if (isOpen && !entityId && !defaultEntityId) { if (isOpen && !entityId && !defaultEntityId) {
@ -242,15 +260,38 @@ export const FileUploadModal = ({
// ── Add files ── // ── Add files ──
const addFiles = useCallback((incoming: File[]) => { 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 const newEntries: FileEntry[] = incoming
.slice(0, MAX_FILES) .slice(0, MAX_FILES)
.map((file) => ({ .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, file,
id: `${file.name}-${file.size}-${Date.now()}-${Math.random()}`, id: `${file.name}-${file.size}-${Date.now()}-${Math.random()}`,
status: isBlocked(file.name) ? "blocked" : "idle", status,
progress: 0, progress: 0,
error: isBlocked(file.name) ? "Blocked file type" : undefined, error,
})); };
});
setFileEntries((prev) => { setFileEntries((prev) => {
const combined = [...prev, ...newEntries]; const combined = [...prev, ...newEntries];
@ -259,7 +300,7 @@ export const FileUploadModal = ({
setValue("files", limited.map(e => e.file), { shouldValidate: true }); setValue("files", limited.map(e => e.file), { shouldValidate: true });
return limited; return limited;
}); });
}, [setValue]); }, [setValue, quota]);
const onDrop = useCallback( const onDrop = useCallback(
(e: DragEvent<HTMLDivElement>) => { (e: DragEvent<HTMLDivElement>) => {
@ -433,7 +474,9 @@ export const FileUploadModal = ({
<p className="text-xs text-[#9aa6b2] mt-0.5"> <p className="text-xs text-[#9aa6b2] mt-0.5">
Attach supporting source files via File Attachment Service Attach supporting source files via File Attachment Service
</p> </p>
<p className="text-xs text-[#9aa6b2]">PDF, DOCX, XLSX up to {MAX_SIZE_MB}MB</p> <p className="text-xs text-[#9aa6b2]">
Allowed formats up to {quota ? formatBytes(quota.max_file_size_bytes) : `${MAX_SIZE_MB}MB`}
</p>
</div> </div>
</div> </div>
) : ( ) : (

View File

@ -28,8 +28,8 @@ const assignmentSchema = z.object({
const newUserSchema = z const newUserSchema = z
.object({ .object({
email: z.email({ message: "Please enter a valid email address" }), email: z.email({ message: "Please enter a valid email address" }),
first_name: z.string().min(1, "First 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"), 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" }), status: z.enum(["active", "suspended", "deleted"], { message: "Status is required" }),
auth_provider: z.enum(["local"], { message: "Auth provider 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") role_module_assignments: z.array(assignmentSchema).min(1, "At least one role assignment is required")

View File

@ -145,7 +145,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-[15px] font-bold gap-1 h-7" className="text-[15px] font-bold gap-1 h-7 cursor-pointer"
style={{ color: primaryColor }} style={{ color: primaryColor }}
onClick={() => navigate(auditLogPath)} onClick={() => navigate(auditLogPath)}
> >
@ -214,7 +214,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
className="h-8 text-[11px] gap-1.5 border-[rgba(0,0,0,0.08)] hover:bg-gray-50" className="h-8 text-[11px] gap-1.5 border-[rgba(0,0,0,0.08)] hover:bg-gray-50 cursor-pointer"
onClick={() => navigate(auditLogPath)} onClick={() => navigate(auditLogPath)}
> >
View All <ArrowRight className="w-3 h-3" /> View All <ArrowRight className="w-3 h-3" />

View File

@ -59,11 +59,16 @@ const tenantDetailsSchema = z.object({
}); });
// Step 2: Contact Details Schema - user creation + organization address // Step 2: Contact Details Schema - user creation + organization address
const contactDetailsSchema = z const contactDetailsSchema = z.object({
.object({
email: z.email({ message: "Please enter a valid email address" }), email: z.email({ message: "Please enter a valid email address" }),
first_name: z.string().min(1, "First name is required"), first_name: z
last_name: z.string().min(1, "Last name is required"), .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"),
contact_phone: z contact_phone: z
.string() .string()
.optional() .optional()

View File

@ -68,8 +68,14 @@ const tenantDetailsSchema = z.object({
const contactDetailsSchema = z.object({ const contactDetailsSchema = z.object({
id: z.string().uuid().optional().nullable(), id: z.string().uuid().optional().nullable(),
email: z.string().email({ message: "Please enter a valid email address" }), email: z.string().email({ message: "Please enter a valid email address" }),
first_name: z.string().min(1, "First name is required"), first_name: z
last_name: z.string().min(1, "Last name is required"), .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"),
contact_phone: z contact_phone: z
.string() .string()
.optional() .optional()

View File

@ -149,6 +149,8 @@ const StorageDashboard = (): ReactElement => {
fileAttachmentService.getStorageStats(), fileAttachmentService.getStorageStats(),
fileAttachmentService.getQuota(), fileAttachmentService.getQuota(),
]); ]);
console.log(quotaRes.data);
setStats(statsRes.data); setStats(statsRes.data);
setQuota(quotaRes.data); setQuota(quotaRes.data);
} catch (err: any) { } catch (err: any) {