refactor: enhance user validation schemas, improve code formatting, and debug storage statistics retrieval.
This commit is contained in:
parent
793fa23c1b
commit
53c4048ad5
@ -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<string>();
|
||||
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<string, string[]>();
|
||||
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 = ({
|
||||
|
||||
<div className="mt-2 mb-4">
|
||||
<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
|
||||
type="button"
|
||||
onClick={() => append({ role_id: "", module_ids: [] })}
|
||||
@ -636,7 +685,7 @@ export const EditUserModal = ({
|
||||
<Plus className="w-4 h-4" /> Add Assignment
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{(errors.role_module_assignments as any)?.message && (
|
||||
<p className="text-sm text-red-500 mb-2">
|
||||
{(errors.role_module_assignments as any).message}
|
||||
@ -645,41 +694,76 @@ export const EditUserModal = ({
|
||||
|
||||
<div className="space-y-3">
|
||||
{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 (
|
||||
<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">
|
||||
<PaginatedSelect
|
||||
label="Select Role"
|
||||
required
|
||||
placeholder="Select Role"
|
||||
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}
|
||||
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
|
||||
}
|
||||
/>
|
||||
<MultiselectPaginatedSelect
|
||||
label="Select Modules"
|
||||
required
|
||||
placeholder="Select Modules"
|
||||
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}
|
||||
initialOptions={initialSelectedModules}
|
||||
error={getModuleError(index)}
|
||||
|
||||
@ -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<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 ──
|
||||
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<HTMLDivElement>) => {
|
||||
@ -433,7 +474,9 @@ export const FileUploadModal = ({
|
||||
<p className="text-xs text-[#9aa6b2] mt-0.5">
|
||||
Attach supporting source files via File Attachment Service
|
||||
</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>
|
||||
) : (
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -145,7 +145,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
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 }}
|
||||
onClick={() => navigate(auditLogPath)}
|
||||
>
|
||||
@ -214,7 +214,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
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)}
|
||||
>
|
||||
View All <ArrowRight className="w-3 h-3" />
|
||||
|
||||
@ -59,33 +59,38 @@ const tenantDetailsSchema = z.object({
|
||||
});
|
||||
|
||||
// Step 2: Contact Details Schema - user creation + organization address
|
||||
const contactDetailsSchema = 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"),
|
||||
contact_phone: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val || val.trim() === "") return true; // Optional field, empty is valid
|
||||
return /^\d{10}$/.test(val);
|
||||
},
|
||||
{
|
||||
message: "Phone number must be exactly 10 digits",
|
||||
},
|
||||
),
|
||||
address_line1: z.string().min(1, "Address is required"),
|
||||
address_line2: z.string().optional().nullable(),
|
||||
city: z.string().min(1, "City is required"),
|
||||
state: z.string().min(1, "State is required"),
|
||||
postal_code: z
|
||||
.string()
|
||||
.regex(/^[1-9]\d{5}$/, "Postal code must be a valid 6-digit PIN code"),
|
||||
country: z.string().min(1, "Country is required"),
|
||||
});
|
||||
const contactDetailsSchema = z.object({
|
||||
email: z.email({ message: "Please enter a valid email address" }),
|
||||
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"),
|
||||
contact_phone: z
|
||||
.string()
|
||||
.optional()
|
||||
.nullable()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val || val.trim() === "") return true; // Optional field, empty is valid
|
||||
return /^\d{10}$/.test(val);
|
||||
},
|
||||
{
|
||||
message: "Phone number must be exactly 10 digits",
|
||||
},
|
||||
),
|
||||
address_line1: z.string().min(1, "Address is required"),
|
||||
address_line2: z.string().optional().nullable(),
|
||||
city: z.string().min(1, "City is required"),
|
||||
state: z.string().min(1, "State is required"),
|
||||
postal_code: z
|
||||
.string()
|
||||
.regex(/^[1-9]\d{5}$/, "Postal code must be a valid 6-digit PIN code"),
|
||||
country: z.string().min(1, "Country is required"),
|
||||
});
|
||||
|
||||
// Step 3: Settings Schema
|
||||
const settingsSchema = z.object({
|
||||
|
||||
@ -68,8 +68,14 @@ const tenantDetailsSchema = z.object({
|
||||
const contactDetailsSchema = z.object({
|
||||
id: z.string().uuid().optional().nullable(),
|
||||
email: z.string().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"),
|
||||
contact_phone: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@ -149,6 +149,8 @@ const StorageDashboard = (): ReactElement => {
|
||||
fileAttachmentService.getStorageStats(),
|
||||
fileAttachmentService.getQuota(),
|
||||
]);
|
||||
console.log(quotaRes.data);
|
||||
|
||||
setStats(statsRes.data);
|
||||
setQuota(quotaRes.data);
|
||||
} catch (err: any) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user