feat: Remove user deletion functionality, add module association to user types, and refactor tenant object out of auth state.

This commit is contained in:
Yashwin 2026-03-16 17:58:11 +05:30
parent 1025504a55
commit 5a0da32699
17 changed files with 776 additions and 633 deletions

View File

@ -1,79 +1,87 @@
import { useEffect, useState, useRef, useMemo } from 'react';
import type { ReactElement } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Loader2, ChevronDown, ChevronRight } from 'lucide-react';
import { Modal, FormField, PrimaryButton, SecondaryButton, MultiselectPaginatedSelect } from '@/components/shared';
import type { Role, UpdateRoleRequest } from '@/types/role';
import { useAppSelector } from '@/hooks/redux-hooks';
import { moduleService } from '@/services/module-service';
import { useEffect, useState, useRef, useMemo } from "react";
import type { ReactElement } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Loader2, ChevronDown, ChevronRight } from "lucide-react";
import {
Modal,
FormField,
PrimaryButton,
SecondaryButton,
} from "@/components/shared";
import type { Role, UpdateRoleRequest } from "@/types/role";
import { useAppSelector } from "@/hooks/redux-hooks";
// Utility function to generate code from name
const generateCodeFromName = (name: string): string => {
return name
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s]/g, '') // Remove special characters except spaces
.replace(/\s+/g, '_') // Replace spaces with underscores
.replace(/_+/g, '_') // Replace multiple underscores with single underscore
.replace(/^_|_$/g, ''); // Remove leading/trailing underscores
.replace(/[^a-z0-9\s]/g, "") // Remove special characters except spaces
.replace(/\s+/g, "_") // Replace spaces with underscores
.replace(/_+/g, "_") // Replace multiple underscores with single underscore
.replace(/^_|_$/g, ""); // Remove leading/trailing underscores
};
// All available resources
const ALL_RESOURCES = [
'users',
"users",
// 'tenants',
'roles',
'permissions',
"roles",
"permissions",
// 'user_roles',
// 'user_tenants',
// 'sessions',
// 'api_keys',
// 'api_key_permissions',
'projects',
'document',
'audit',
'security',
'workflow',
'training',
'capa',
'supplier',
'reports',
'notifications',
'files',
'settings',
"projects",
"document",
"audit",
"security",
"workflow",
"training",
"capa",
"supplier",
"reports",
"notifications",
"files",
"settings",
// 'modules',
'audit_logs',
"audit_logs",
// 'event_logs',
// 'health_history',
'qms_connections',
'qms_sync_jobs',
'qms_sync_conflicts',
'qms_entity_mappings',
'ai',
'qms',
"qms_connections",
"qms_sync_jobs",
"qms_sync_conflicts",
"qms_entity_mappings",
"ai",
"qms",
];
// All available actions
const ALL_ACTIONS = ['create', 'read', 'update', 'delete'];
const ALL_ACTIONS = ["create", "read", "update", "delete"];
// Validation schema
const editRoleSchema = z.object({
name: z.string().min(1, 'Role name is required'),
name: z.string().min(1, "Role name is required"),
code: z
.string()
.min(1, 'Code is required')
.min(1, "Code is required")
.regex(
/^[a-z]+(_[a-z]+)*$/,
"Code must be lowercase and use '_' for separation (e.g. abc_def)"
"Code must be lowercase and use '_' for separation (e.g. abc_def)",
),
description: z.string().min(1, 'Description is required'),
modules: z.array(z.uuid()).optional().nullable(),
permissions: z.array(z.object({
resource: z.string(),
action: z.string(),
})).optional().nullable(),
description: z.string().min(1, "Description is required"),
permissions: z
.array(
z.object({
resource: z.string(),
action: z.string(),
}),
)
.optional()
.nullable(),
});
type EditRoleFormData = z.infer<typeof editRoleSchema>;
@ -103,11 +111,13 @@ export const EditRoleModal = ({
const permissions = useAppSelector((state) => state.auth.permissions);
const roles = useAppSelector((state) => state.auth.roles);
const tenantIdFromAuth = useAppSelector((state) => state.auth.tenantId);
const isSuperAdmin = roles.includes('super_admin');
const [selectedAvailableModules, setSelectedAvailableModules] = useState<string[]>([]);
const [selectedPermissions, setSelectedPermissions] = useState<Array<{ resource: string; action: string }>>([]);
const [initialAvailableModuleOptions, setInitialAvailableModuleOptions] = useState<Array<{ value: string; label: string }>>([]);
const [expandedResources, setExpandedResources] = useState<Set<string>>(new Set());
const isSuperAdmin = roles.includes("super_admin");
const [selectedPermissions, setSelectedPermissions] = useState<
Array<{ resource: string; action: string }>
>([]);
const [expandedResources, setExpandedResources] = useState<Set<string>>(
new Set(),
);
const {
register,
@ -121,36 +131,20 @@ export const EditRoleModal = ({
} = useForm<EditRoleFormData>({
resolver: zodResolver(editRoleSchema),
defaultValues: {
modules: [],
permissions: [],
},
});
const nameValue = watch('name');
const nameValue = watch("name");
// Auto-generate code from name
useEffect(() => {
if (nameValue) {
const generatedCode = generateCodeFromName(nameValue);
setValue('code', generatedCode, { shouldValidate: true });
setValue("code", generatedCode, { shouldValidate: true });
}
}, [nameValue, setValue]);
// Load available modules from /modules/available endpoint
// For super_admin, send tenant_id if defaultTenantId is provided
// For tenant users, send tenant_id from auth state
const loadAvailableModules = async (page: number, limit: number) => {
const tenantId = isSuperAdmin ? defaultTenantId : tenantIdFromAuth;
const response = await moduleService.getAvailable(page, limit, tenantId);
return {
options: response.data.map((module) => ({
value: module.id,
label: module.name,
})),
pagination: response.pagination,
};
};
// Build available resources and actions based on user permissions
const availableResourcesAndActions = useMemo(() => {
const resourceMap = new Map<string, Set<string>>();
@ -158,14 +152,14 @@ export const EditRoleModal = ({
permissions.forEach((perm) => {
const { resource, action } = perm;
if (resource === '*') {
if (resource === "*") {
// If resource is *, show all resources
ALL_RESOURCES.forEach((res) => {
if (!resourceMap.has(res)) {
resourceMap.set(res, new Set());
}
const actions = resourceMap.get(res)!;
if (action === '*') {
if (action === "*") {
// If action is also *, add all actions
ALL_ACTIONS.forEach((act) => actions.add(act));
} else {
@ -178,7 +172,7 @@ export const EditRoleModal = ({
resourceMap.set(resource, new Set());
}
const actions = resourceMap.get(resource)!;
if (action === '*') {
if (action === "*") {
// If action is *, add all actions for this resource
ALL_ACTIONS.forEach((act) => actions.add(act));
} else {
@ -191,17 +185,20 @@ export const EditRoleModal = ({
}, [permissions]);
// Check if a resource has any selected actions
const hasSelectedActions = (resource: string, actions: Set<string>): boolean => {
const hasSelectedActions = (
resource: string,
actions: Set<string>,
): boolean => {
return Array.from(actions).some((action) => {
return selectedPermissions.some((p) => {
// Check for exact match
if (p.resource === resource && p.action === action) return true;
// Check for wildcard resource with exact action
if (p.resource === '*' && p.action === action) return true;
if (p.resource === "*" && p.action === action) return true;
// Check for exact resource with wildcard action
if (p.resource === resource && p.action === '*') return true;
if (p.resource === resource && p.action === "*") return true;
// Check for wildcard resource with wildcard action
if (p.resource === '*' && p.action === '*') return true;
if (p.resource === "*" && p.action === "*") return true;
return false;
});
});
@ -221,17 +218,25 @@ export const EditRoleModal = ({
};
// Handle permission checkbox change
const handlePermissionChange = (resource: string, action: string, checked: boolean) => {
const handlePermissionChange = (
resource: string,
action: string,
checked: boolean,
) => {
setSelectedPermissions((prev) => {
const newPerms = [...prev];
if (checked) {
// Add permission if not already exists
if (!newPerms.some((p) => p.resource === resource && p.action === action)) {
if (
!newPerms.some((p) => p.resource === resource && p.action === action)
) {
newPerms.push({ resource, action });
}
} else {
// Remove permission
return newPerms.filter((p) => !(p.resource === resource && p.action === action));
return newPerms.filter(
(p) => !(p.resource === resource && p.action === action),
);
}
return newPerms;
});
@ -239,20 +244,21 @@ export const EditRoleModal = ({
// Update form value when permissions change
useEffect(() => {
setValue('permissions', selectedPermissions.length > 0 ? selectedPermissions : []);
setValue(
"permissions",
selectedPermissions.length > 0 ? selectedPermissions : [],
);
}, [selectedPermissions, setValue]);
// Update form value when available modules change
useEffect(() => {
setValue('modules', selectedAvailableModules.length > 0 ? selectedAvailableModules : []);
}, [selectedAvailableModules, setValue]);
// Expand resources that have selected permissions when role is loaded
useEffect(() => {
if (selectedPermissions.length > 0 && availableResourcesAndActions.size > 0) {
if (
selectedPermissions.length > 0 &&
availableResourcesAndActions.size > 0
) {
const resourcesWithPermissions = new Set<string>();
selectedPermissions.forEach((perm) => {
if (perm.resource === '*') {
if (perm.resource === "*") {
// If wildcard resource, expand all available resources
availableResourcesAndActions.forEach((_, resource) => {
resourcesWithPermissions.add(resource);
@ -277,7 +283,6 @@ export const EditRoleModal = ({
}
}, [selectedPermissions, availableResourcesAndActions]);
// Load role data when modal opens - only load once per roleId
useEffect(() => {
if (isOpen && roleId) {
@ -291,71 +296,10 @@ export const EditRoleModal = ({
const role = await onLoadRole(roleId);
loadedRoleIdRef.current = roleId;
// Extract modules and permissions from role
const roleModules = role.modules || [];
const rolePermissions = role.permissions || [];
// Set available modules if exists
if (roleModules.length > 0) {
setSelectedAvailableModules(roleModules);
setValue('modules', roleModules);
// Load module names from available modules API
// Use tenant_id from auth for tenant users, or defaultTenantId for super_admin
const loadModuleNames = async () => {
try {
const tenantId = isSuperAdmin ? defaultTenantId : tenantIdFromAuth;
// Load first page of available modules to get module names
const availableModulesResponse = await moduleService.getAvailable(1, 100, tenantId);
// Map role modules to options from available modules
const moduleOptions = roleModules
.map((moduleId: string) => {
const module = availableModulesResponse.data.find((m) => m.id === moduleId);
if (module) {
return {
value: moduleId,
label: module.name,
};
}
return null;
})
.filter((opt) => opt !== null) as Array<{ value: string; label: string }>;
setInitialAvailableModuleOptions(moduleOptions);
} catch (err) {
console.warn('Failed to load available module names:', err);
// Fallback: try to load individual modules if available modules endpoint fails
try {
const moduleOptionsPromises = roleModules.map(async (moduleId: string) => {
try {
const moduleResponse = await moduleService.getById(moduleId);
return {
value: moduleId,
label: moduleResponse.data.name,
};
} catch {
return null;
}
});
const moduleOptions = (await Promise.all(moduleOptionsPromises)).filter(
(opt) => opt !== null
) as Array<{ value: string; label: string }>;
setInitialAvailableModuleOptions(moduleOptions);
} catch (fallbackErr) {
console.warn('Fallback loading also failed:', fallbackErr);
}
}
};
loadModuleNames();
} else {
setSelectedAvailableModules([]);
setInitialAvailableModuleOptions([]);
}
// Set permissions (always set, even if empty array)
const rolePermissions = role.permissions || [];
setSelectedPermissions(rolePermissions);
setValue('permissions', rolePermissions);
setValue("permissions", rolePermissions);
// Expand resources that have selected permissions
// This will be handled by useEffect after availableResourcesAndActions is computed
@ -363,12 +307,14 @@ export const EditRoleModal = ({
reset({
name: role.name,
code: role.code,
description: role.description || '',
modules: roleModules,
description: role.description || "",
permissions: rolePermissions,
});
} catch (err: any) {
setLoadError(err?.response?.data?.error?.message || 'Failed to load role details');
setLoadError(
err?.response?.data?.error?.message ||
"Failed to load role details",
);
} finally {
setIsLoadingRole(false);
}
@ -378,67 +324,86 @@ export const EditRoleModal = ({
} else if (!isOpen) {
// Only reset when modal is closed
loadedRoleIdRef.current = null;
setSelectedAvailableModules([]);
setSelectedPermissions([]);
setInitialAvailableModuleOptions([]);
reset({
name: '',
code: '',
description: '',
modules: [],
name: "",
code: "",
description: "",
permissions: [],
});
setLoadError(null);
clearErrors();
}
}, [isOpen, roleId, onLoadRole, reset, clearErrors, setValue, isSuperAdmin, defaultTenantId, tenantIdFromAuth]);
}, [
isOpen,
roleId,
onLoadRole,
reset,
clearErrors,
setValue,
isSuperAdmin,
defaultTenantId,
tenantIdFromAuth,
]);
const handleFormSubmit = async (data: EditRoleFormData): Promise<void> => {
if (!roleId) return;
clearErrors();
try {
const submitData = {
...data,
// For super_admin, always include tenant_id if defaultTenantId is provided
tenant_id: isSuperAdmin ? (defaultTenantId || undefined) : (defaultTenantId || undefined),
// Include modules from available modules endpoint
modules: selectedAvailableModules.length > 0 ? selectedAvailableModules : undefined,
permissions: selectedPermissions.length > 0 ? selectedPermissions : undefined,
tenant_id: isSuperAdmin
? defaultTenantId || undefined
: defaultTenantId || undefined,
permissions:
selectedPermissions.length > 0 ? selectedPermissions : undefined,
};
await onSubmit(roleId, submitData as UpdateRoleRequest);
// Only reset form on success - this will be handled by parent closing modal
} catch (error: any) {
// Don't reset form on error - keep the form data and show errors
// Handle validation errors from API
if (error?.response?.data?.details && Array.isArray(error.response.data.details)) {
if (
error?.response?.data?.details &&
Array.isArray(error.response.data.details)
) {
const validationErrors = error.response.data.details;
validationErrors.forEach((detail: { path: string; message: string }) => {
if (
detail.path === 'name' ||
detail.path === 'code' ||
detail.path === 'description' ||
detail.path === 'modules' ||
detail.path === 'permissions'
) {
setError(detail.path as keyof EditRoleFormData, {
type: 'server',
message: detail.message,
});
}
});
validationErrors.forEach(
(detail: { path: string; message: string }) => {
if (
detail.path === "name" ||
detail.path === "code" ||
detail.path === "description" ||
detail.path === "permissions"
) {
setError(detail.path as keyof EditRoleFormData, {
type: "server",
message: detail.message,
});
}
},
);
} else {
// Handle general errors
const errorObj = error?.response?.data?.error;
const errorMessage =
(typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) ||
(typeof errorObj === 'string' ? errorObj : null) ||
error?.response?.data?.message ||
error?.message ||
'Failed to update role. Please try again.';
setError('root', {
type: 'server',
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to update role. Please try again.',
const errorMessage =
(typeof errorObj === "object" &&
errorObj !== null &&
"message" in errorObj
? errorObj.message
: null) ||
(typeof errorObj === "string" ? errorObj : null) ||
error?.response?.data?.message ||
error?.message ||
"Failed to update role. Please try again.";
setError("root", {
type: "server",
message:
typeof errorMessage === "string"
? errorMessage
: "Failed to update role. Please try again.",
});
}
// Re-throw error to prevent form from thinking it succeeded
@ -470,7 +435,7 @@ export const EditRoleModal = ({
size="default"
className="px-4 py-2.5 text-sm"
>
{isLoading ? 'Updating...' : 'Update Role'}
{isLoading ? "Updating..." : "Update Role"}
</PrimaryButton>
</>
}
@ -495,7 +460,10 @@ export const EditRoleModal = ({
)}
{!isLoadingRole && (
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-5 flex flex-col gap-0">
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-0"
>
{/* Role Name and Role Code Row */}
<div className="grid grid-cols-2 gap-5 pb-4">
<FormField
@ -503,7 +471,7 @@ export const EditRoleModal = ({
required
placeholder="Enter Text Here"
error={errors.name?.message}
{...register('name')}
{...register("name")}
/>
<FormField
@ -511,7 +479,7 @@ export const EditRoleModal = ({
required
placeholder="Auto-generated from name"
error={errors.code?.message}
{...register('code')}
{...register("code")}
disabled
className="bg-[#f3f4f6] cursor-not-allowed"
/>
@ -523,115 +491,134 @@ export const EditRoleModal = ({
required
placeholder="Enter Text Here"
error={errors.description?.message}
{...register('description')}
{...register("description")}
/>
{/* Available Modules Selection */}
<MultiselectPaginatedSelect
label="Available Modules"
placeholder="Select available modules"
value={selectedAvailableModules}
onValueChange={(values) => {
setSelectedAvailableModules(values);
setValue('modules', values.length > 0 ? values : []);
}}
onLoadOptions={loadAvailableModules}
initialOptions={initialAvailableModuleOptions}
error={errors.modules?.message}
/>
{/* Permissions Section */}
<div className="pb-4">
<label className="flex items-center gap-1.5 text-[13px] font-medium text-[#0e1b2a] mb-3">
<span>Permissions</span>
</label>
{errors.permissions && (
<p className="text-sm text-[#ef4444] mb-2">{errors.permissions.message}</p>
<p className="text-sm text-[#ef4444] mb-2">
{errors.permissions.message}
</p>
)}
<div className="border border-[rgba(0,0,0,0.08)] rounded-md p-4">
{Array.from(availableResourcesAndActions.entries()).length === 0 ? (
<p className="text-sm text-[#6b7280]">No permissions available</p>
{Array.from(availableResourcesAndActions.entries()).length ===
0 ? (
<p className="text-sm text-[#6b7280]">
No permissions available
</p>
) : (
<div className="space-y-2">
{Array.from(availableResourcesAndActions.entries()).map(([resource, actions]) => {
const isExpanded = expandedResources.has(resource);
const hasSelected = hasSelectedActions(resource, actions);
return (
<div
key={resource}
className={`border border-[rgba(0,0,0,0.08)] rounded-md overflow-hidden transition-colors ${
hasSelected ? 'bg-[rgba(17,40,104,0.05)]' : 'bg-white'
}`}
>
<button
type="button"
onClick={() => toggleResource(resource)}
className="w-full flex items-center justify-between p-3 hover:bg-[rgba(0,0,0,0.02)] transition-colors"
{Array.from(availableResourcesAndActions.entries()).map(
([resource, actions]) => {
const isExpanded = expandedResources.has(resource);
const hasSelected = hasSelectedActions(resource, actions);
return (
<div
key={resource}
className={`border border-[rgba(0,0,0,0.08)] rounded-md overflow-hidden transition-colors ${
hasSelected
? "bg-[rgba(17,40,104,0.05)]"
: "bg-white"
}`}
>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-[#0e1b2a]" />
) : (
<ChevronRight className="w-4 h-4 text-[#0e1b2a]" />
)}
<span
className={`font-medium text-sm capitalize ${
hasSelected ? 'text-[#112868]' : 'text-[#0e1b2a]'
}`}
>
{resource.replace(/_/g, ' ')}
</span>
{hasSelected && (
<span className="text-xs text-[#112868] bg-[rgba(17,40,104,0.1)] px-2 py-0.5 rounded">
Selected
<button
type="button"
onClick={() => toggleResource(resource)}
className="w-full flex items-center justify-between p-3 hover:bg-[rgba(0,0,0,0.02)] transition-colors"
>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-[#0e1b2a]" />
) : (
<ChevronRight className="w-4 h-4 text-[#0e1b2a]" />
)}
<span
className={`font-medium text-sm capitalize ${
hasSelected
? "text-[#112868]"
: "text-[#0e1b2a]"
}`}
>
{resource.replace(/_/g, " ")}
</span>
)}
</div>
</button>
{isExpanded && (
<div className="px-3 pb-3 pt-2 border-t border-[rgba(0,0,0,0.05)]">
<div className="flex flex-wrap gap-4">
{Array.from(actions).map((action) => {
const isChecked = selectedPermissions.some((p) => {
// Check for exact match
if (p.resource === resource && p.action === action) {
return true;
}
// Check for wildcard resource with exact action
if (p.resource === '*' && p.action === action) {
return true;
}
// Check for exact resource with wildcard action
if (p.resource === resource && p.action === '*') {
return true;
}
// Check for wildcard resource with wildcard action
if (p.resource === '*' && p.action === '*') {
return true;
}
return false;
});
return (
<label
key={`${resource}-${action}`}
className="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
checked={isChecked}
onChange={(e) => handlePermissionChange(resource, action, e.target.checked)}
className="w-4 h-4 text-[#112868] border-[rgba(0,0,0,0.2)] rounded focus:ring-[#112868]/20"
/>
<span className="text-sm text-[#0e1b2a] capitalize">{action}</span>
</label>
);
})}
{hasSelected && (
<span className="text-xs text-[#112868] bg-[rgba(17,40,104,0.1)] px-2 py-0.5 rounded">
Selected
</span>
)}
</div>
</div>
)}
</div>
);
})}
</button>
{isExpanded && (
<div className="px-3 pb-3 pt-2 border-t border-[rgba(0,0,0,0.05)]">
<div className="flex flex-wrap gap-4">
{Array.from(actions).map((action) => {
const isChecked = selectedPermissions.some(
(p) => {
// Check for exact match
if (
p.resource === resource &&
p.action === action
) {
return true;
}
// Check for wildcard resource with exact action
if (
p.resource === "*" &&
p.action === action
) {
return true;
}
// Check for exact resource with wildcard action
if (
p.resource === resource &&
p.action === "*"
) {
return true;
}
// Check for wildcard resource with wildcard action
if (
p.resource === "*" &&
p.action === "*"
) {
return true;
}
return false;
},
);
return (
<label
key={`${resource}-${action}`}
className="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
checked={isChecked}
onChange={(e) =>
handlePermissionChange(
resource,
action,
e.target.checked,
)
}
className="w-4 h-4 text-[#112868] border-[rgba(0,0,0,0.2)] rounded focus:ring-[#112868]/20"
/>
<span className="text-sm text-[#0e1b2a] capitalize">
{action}
</span>
</label>
);
})}
</div>
</div>
)}
</div>
);
},
)}
</div>
)}
</div>

View File

@ -16,6 +16,8 @@ import {
import { roleService } from "@/services/role-service";
import { departmentService } from "@/services/department-service";
import { designationService } from "@/services/designation-service";
import { moduleService } from "@/services/module-service";
import { useAppSelector } from "@/hooks/redux-hooks";
import type { User } from "@/types/user";
// Validation schema
@ -30,6 +32,7 @@ const editUserSchema = z.object({
role_ids: z.array(z.string()).min(1, "At least one role is required"),
department_id: z.string().optional(),
designation_id: z.string().optional(),
module_ids: z.array(z.string()).optional(),
});
type EditUserFormData = z.infer<typeof editUserSchema>;
@ -80,6 +83,11 @@ export const EditUserModal = ({
const roleIdsValue = watch("role_ids");
const departmentIdValue = watch("department_id");
const designationIdValue = watch("designation_id");
const moduleIdsValue = watch("module_ids");
const rolesFromAuth = useAppSelector((state) => state.auth.roles);
const isSuperAdmin = rolesFromAuth.includes("super_admin");
const tenantIdFromAuth = useAppSelector((state) => state.auth.tenantId);
const [initialRoleOptions, setInitialRoleOptions] = useState<
{ value: string; label: string }[]
@ -92,6 +100,9 @@ export const EditUserModal = ({
value: string;
label: string;
} | null>(null);
const [initialModuleOptions, setInitialModuleOptions] = useState<
{ value: string; label: string }[]
>([]);
// Load roles for dropdown - ensure selected role is included
const loadRoles = async (page: number, limit: number) => {
@ -145,6 +156,18 @@ export const EditUserModal = ({
};
};
const loadModules = async (page: number, limit: number) => {
const tenantId = isSuperAdmin ? defaultTenantId : tenantIdFromAuth;
const response = await moduleService.getAvailable(page, limit, tenantId);
return {
options: response.data.map((module) => ({
value: module.id,
label: module.name,
})),
pagination: response.pagination,
};
};
// Load user data when modal opens
useEffect(() => {
if (isOpen && userId) {
@ -200,8 +223,15 @@ export const EditUserModal = ({
role_ids: roleIds,
department_id: departmentId,
designation_id: designationId,
module_ids: user.modules?.map((m) => m.id) || [],
});
if (user.modules) {
setInitialModuleOptions(
user.modules.map((m) => ({ value: m.id, label: m.name })),
);
}
if (defaultTenantId) {
setValue("tenant_id", defaultTenantId, { shouldValidate: true });
}
@ -230,7 +260,9 @@ export const EditUserModal = ({
role_ids: [],
department_id: "",
designation_id: "",
module_ids: [],
});
setInitialModuleOptions([]);
setLoadError(null);
clearErrors();
}
@ -412,6 +444,18 @@ export const EditUserModal = ({
error={errors.status?.message}
/>
</div>
<div className="pb-4">
<MultiselectPaginatedSelect
label="Assign Modules"
placeholder="Select Modules"
value={moduleIdsValue || []}
onValueChange={(value) => setValue("module_ids", value)}
onLoadOptions={loadModules}
initialOptions={initialModuleOptions}
error={errors.module_ids?.message}
/>
</div>
</div>
)}
</form>

View File

@ -1,79 +1,87 @@
import { useEffect, useState, useMemo } from 'react';
import type { ReactElement } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { Modal, FormField, PrimaryButton, SecondaryButton, MultiselectPaginatedSelect } from '@/components/shared';
import type { CreateRoleRequest } from '@/types/role';
import { useAppSelector } from '@/hooks/redux-hooks';
import { moduleService } from '@/services/module-service';
import { useEffect, useState, useMemo } from "react";
import type { ReactElement } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { ChevronDown, ChevronRight } from "lucide-react";
import {
Modal,
FormField,
PrimaryButton,
SecondaryButton,
} from "@/components/shared";
import type { CreateRoleRequest } from "@/types/role";
import { useAppSelector } from "@/hooks/redux-hooks";
// Utility function to generate code from name
const generateCodeFromName = (name: string): string => {
return name
.toLowerCase()
.trim()
.replace(/[^a-z0-9\s]/g, '') // Remove special characters except spaces
.replace(/\s+/g, '_') // Replace spaces with underscores
.replace(/_+/g, '_') // Replace multiple underscores with single underscore
.replace(/^_|_$/g, ''); // Remove leading/trailing underscores
.replace(/[^a-z0-9\s]/g, "") // Remove special characters except spaces
.replace(/\s+/g, "_") // Replace spaces with underscores
.replace(/_+/g, "_") // Replace multiple underscores with single underscore
.replace(/^_|_$/g, ""); // Remove leading/trailing underscores
};
// All available resources
const ALL_RESOURCES = [
'users',
"users",
// 'tenants',
'roles',
'permissions',
"roles",
"permissions",
// 'user_roles',
// 'user_tenants',
// 'sessions',
// 'api_keys',
// 'api_key_permissions',
'projects',
'document',
'audit',
'security',
'workflow',
'training',
'capa',
'supplier',
'reports',
'notifications',
'files',
'settings',
"projects",
"document",
"audit",
"security",
"workflow",
"training",
"capa",
"supplier",
"reports",
"notifications",
"files",
"settings",
// 'modules',
'audit_logs',
"audit_logs",
// 'event_logs',
// 'health_history',
'qms_connections',
'qms_sync_jobs',
'qms_sync_conflicts',
'qms_entity_mappings',
'ai',
'qms',
"qms_connections",
"qms_sync_jobs",
"qms_sync_conflicts",
"qms_entity_mappings",
"ai",
"qms",
];
// All available actions
const ALL_ACTIONS = ['create', 'read', 'update', 'delete'];
const ALL_ACTIONS = ["create", "read", "update", "delete"];
// Validation schema
const newRoleSchema = z.object({
name: z.string().min(1, 'Role name is required'),
name: z.string().min(1, "Role name is required"),
code: z
.string()
.min(1, 'Code is required')
.min(1, "Code is required")
.regex(
/^[a-z]+(_[a-z]+)*$/,
"Code must be lowercase and use '_' for separation (e.g. abc_def)"
"Code must be lowercase and use '_' for separation (e.g. abc_def)",
),
description: z.string().min(1, 'Description is required'),
modules: z.array(z.uuid()).optional().nullable(),
permissions: z.array(z.object({
resource: z.string(),
action: z.string(),
})).optional().nullable(),
description: z.string().min(1, "Description is required"),
permissions: z
.array(
z.object({
resource: z.string(),
action: z.string(),
}),
)
.optional()
.nullable(),
});
type NewRoleFormData = z.infer<typeof newRoleSchema>;
@ -95,10 +103,13 @@ export const NewRoleModal = ({
}: NewRoleModalProps): ReactElement | null => {
const permissions = useAppSelector((state) => state.auth.permissions);
const roles = useAppSelector((state) => state.auth.roles);
const isSuperAdmin = roles.includes('super_admin');
const [selectedAvailableModules, setSelectedAvailableModules] = useState<string[]>([]);
const [selectedPermissions, setSelectedPermissions] = useState<Array<{ resource: string; action: string }>>([]);
const [expandedResources, setExpandedResources] = useState<Set<string>>(new Set());
const isSuperAdmin = roles.includes("super_admin");
const [selectedPermissions, setSelectedPermissions] = useState<
Array<{ resource: string; action: string }>
>([]);
const [expandedResources, setExpandedResources] = useState<Set<string>>(
new Set(),
);
const {
register,
@ -113,18 +124,17 @@ export const NewRoleModal = ({
resolver: zodResolver(newRoleSchema),
defaultValues: {
code: undefined,
modules: [],
permissions: [],
},
});
const nameValue = watch('name');
const nameValue = watch("name");
// Auto-generate code from name
useEffect(() => {
if (nameValue) {
const generatedCode = generateCodeFromName(nameValue);
setValue('code', generatedCode, { shouldValidate: true });
setValue("code", generatedCode, { shouldValidate: true });
}
}, [nameValue, setValue]);
@ -132,32 +142,16 @@ export const NewRoleModal = ({
useEffect(() => {
if (!isOpen) {
reset({
name: '',
name: "",
code: undefined,
description: '',
modules: [],
description: "",
permissions: [],
});
setSelectedAvailableModules([]);
setSelectedPermissions([]);
clearErrors();
}
}, [isOpen, reset, clearErrors]);
// Load available modules from /modules/available endpoint
// For super_admin, send tenant_id if defaultTenantId is provided
const loadAvailableModules = async (page: number, limit: number) => {
const tenantId = isSuperAdmin ? defaultTenantId : undefined;
const response = await moduleService.getAvailable(page, limit, tenantId);
return {
options: response.data.map((module) => ({
value: module.id,
label: module.name,
})),
pagination: response.pagination,
};
};
// Build available resources and actions based on user permissions
const availableResourcesAndActions = useMemo(() => {
const resourceMap = new Map<string, Set<string>>();
@ -165,14 +159,14 @@ export const NewRoleModal = ({
permissions.forEach((perm) => {
const { resource, action } = perm;
if (resource === '*') {
if (resource === "*") {
// If resource is *, show all resources
ALL_RESOURCES.forEach((res) => {
if (!resourceMap.has(res)) {
resourceMap.set(res, new Set());
}
const actions = resourceMap.get(res)!;
if (action === '*') {
if (action === "*") {
// If action is also *, add all actions
ALL_ACTIONS.forEach((act) => actions.add(act));
} else {
@ -185,7 +179,7 @@ export const NewRoleModal = ({
resourceMap.set(resource, new Set());
}
const actions = resourceMap.get(resource)!;
if (action === '*') {
if (action === "*") {
// If action is *, add all actions for this resource
ALL_ACTIONS.forEach((act) => actions.add(act));
} else {
@ -198,17 +192,20 @@ export const NewRoleModal = ({
}, [permissions]);
// Check if a resource has any selected actions
const hasSelectedActions = (resource: string, actions: Set<string>): boolean => {
const hasSelectedActions = (
resource: string,
actions: Set<string>,
): boolean => {
return Array.from(actions).some((action) => {
return selectedPermissions.some((p) => {
// Check for exact match
if (p.resource === resource && p.action === action) return true;
// Check for wildcard resource with exact action
if (p.resource === '*' && p.action === action) return true;
if (p.resource === "*" && p.action === action) return true;
// Check for exact resource with wildcard action
if (p.resource === resource && p.action === '*') return true;
if (p.resource === resource && p.action === "*") return true;
// Check for wildcard resource with wildcard action
if (p.resource === '*' && p.action === '*') return true;
if (p.resource === "*" && p.action === "*") return true;
return false;
});
});
@ -228,17 +225,25 @@ export const NewRoleModal = ({
};
// Handle permission checkbox change
const handlePermissionChange = (resource: string, action: string, checked: boolean) => {
const handlePermissionChange = (
resource: string,
action: string,
checked: boolean,
) => {
setSelectedPermissions((prev) => {
const newPerms = [...prev];
if (checked) {
// Add permission if not already exists
if (!newPerms.some((p) => p.resource === resource && p.action === action)) {
if (
!newPerms.some((p) => p.resource === resource && p.action === action)
) {
newPerms.push({ resource, action });
}
} else {
// Remove permission
return newPerms.filter((p) => !(p.resource === resource && p.action === action));
return newPerms.filter(
(p) => !(p.resource === resource && p.action === action),
);
}
return newPerms;
});
@ -246,56 +251,66 @@ export const NewRoleModal = ({
// Update form value when permissions change
useEffect(() => {
setValue('permissions', selectedPermissions.length > 0 ? selectedPermissions : []);
setValue(
"permissions",
selectedPermissions.length > 0 ? selectedPermissions : [],
);
}, [selectedPermissions, setValue]);
// Update form value when available modules change
useEffect(() => {
setValue('modules', selectedAvailableModules.length > 0 ? selectedAvailableModules : []);
}, [selectedAvailableModules, setValue]);
const handleFormSubmit = async (data: NewRoleFormData): Promise<void> => {
clearErrors();
try {
const submitData = {
...data,
// For super_admin, always include tenant_id if defaultTenantId is provided
tenant_id: isSuperAdmin ? (defaultTenantId || undefined) : (defaultTenantId || undefined),
// Include modules from available modules endpoint
modules: selectedAvailableModules.length > 0 ? selectedAvailableModules : undefined,
permissions: selectedPermissions.length > 0 ? selectedPermissions : undefined,
tenant_id: isSuperAdmin
? defaultTenantId || undefined
: defaultTenantId || undefined,
permissions:
selectedPermissions.length > 0 ? selectedPermissions : undefined,
};
await onSubmit(submitData as CreateRoleRequest);
} catch (error: any) {
// Handle validation errors from API
if (error?.response?.data?.details && Array.isArray(error.response.data.details)) {
if (
error?.response?.data?.details &&
Array.isArray(error.response.data.details)
) {
const validationErrors = error.response.data.details;
validationErrors.forEach((detail: { path: string; message: string }) => {
if (
detail.path === 'name' ||
detail.path === 'code' ||
detail.path === 'description' ||
detail.path === 'modules' ||
detail.path === 'permissions'
) {
setError(detail.path as keyof NewRoleFormData, {
type: 'server',
message: detail.message,
});
}
});
validationErrors.forEach(
(detail: { path: string; message: string }) => {
if (
detail.path === "name" ||
detail.path === "code" ||
detail.path === "description" ||
detail.path === "permissions"
) {
setError(detail.path as keyof NewRoleFormData, {
type: "server",
message: detail.message,
});
}
},
);
} else {
// Handle general errors
const errorObj = error?.response?.data?.error;
const errorMessage =
(typeof errorObj === 'object' && errorObj !== null && 'message' in errorObj ? errorObj.message : null) ||
(typeof errorObj === 'string' ? errorObj : null) ||
error?.response?.data?.message ||
error?.message ||
'Failed to create role. Please try again.';
setError('root', {
type: 'server',
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to create role. Please try again.',
const errorMessage =
(typeof errorObj === "object" &&
errorObj !== null &&
"message" in errorObj
? errorObj.message
: null) ||
(typeof errorObj === "string" ? errorObj : null) ||
error?.response?.data?.message ||
error?.message ||
"Failed to create role. Please try again.";
setError("root", {
type: "server",
message:
typeof errorMessage === "string"
? errorMessage
: "Failed to create role. Please try again.",
});
}
}
@ -325,12 +340,15 @@ export const NewRoleModal = ({
size="default"
className="px-4 py-2.5 text-sm"
>
{isLoading ? 'Creating...' : 'Create Role'}
{isLoading ? "Creating..." : "Create Role"}
</PrimaryButton>
</>
}
>
<form onSubmit={handleSubmit(handleFormSubmit)} className="p-5 flex flex-col gap-0">
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="p-5 flex flex-col gap-0"
>
{/* General Error Display */}
{errors.root && (
<div className="p-4 bg-[rgba(239,68,68,0.1)] border border-[#ef4444] rounded-md mb-4">
@ -345,7 +363,7 @@ export const NewRoleModal = ({
required
placeholder="Enter Text Here"
error={errors.name?.message}
{...register('name')}
{...register("name")}
/>
<FormField
@ -353,7 +371,7 @@ export const NewRoleModal = ({
required
placeholder="Auto-generated from name"
error={errors.code?.message}
{...register('code')}
{...register("code")}
disabled
className="bg-[#f3f4f6] cursor-not-allowed"
/>
@ -365,106 +383,122 @@ export const NewRoleModal = ({
required
placeholder="Enter Text Here"
error={errors.description?.message}
{...register('description')}
{...register("description")}
/>
{/* Available Modules Selection */}
<MultiselectPaginatedSelect
label="Available Modules"
placeholder="Select available modules"
value={selectedAvailableModules}
onValueChange={(values) => {
setSelectedAvailableModules(values);
setValue('modules', values.length > 0 ? values : []);
}}
onLoadOptions={loadAvailableModules}
error={errors.modules?.message}
/>
{/* Permissions Section */}
<div className="pb-4">
<label className="flex items-center gap-1.5 text-[13px] font-medium text-[#0e1b2a] mb-3">
<span>Permissions</span>
</label>
{errors.permissions && (
<p className="text-sm text-[#ef4444] mb-2">{errors.permissions.message}</p>
<p className="text-sm text-[#ef4444] mb-2">
{errors.permissions.message}
</p>
)}
<div className="border border-[rgba(0,0,0,0.08)] rounded-md p-4">
{Array.from(availableResourcesAndActions.entries()).length === 0 ? (
<p className="text-sm text-[#6b7280]">No permissions available</p>
) : (
<div className="space-y-2">
{Array.from(availableResourcesAndActions.entries()).map(([resource, actions]) => {
const isExpanded = expandedResources.has(resource);
const hasSelected = hasSelectedActions(resource, actions);
return (
<div
key={resource}
className={`border border-[rgba(0,0,0,0.08)] rounded-md overflow-hidden transition-colors ${
hasSelected ? 'bg-[rgba(17,40,104,0.05)]' : 'bg-white'
}`}
>
<button
type="button"
onClick={() => toggleResource(resource)}
className="w-full flex items-center justify-between p-3 hover:bg-[rgba(0,0,0,0.02)] transition-colors"
{Array.from(availableResourcesAndActions.entries()).map(
([resource, actions]) => {
const isExpanded = expandedResources.has(resource);
const hasSelected = hasSelectedActions(resource, actions);
return (
<div
key={resource}
className={`border border-[rgba(0,0,0,0.08)] rounded-md overflow-hidden transition-colors ${
hasSelected ? "bg-[rgba(17,40,104,0.05)]" : "bg-white"
}`}
>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-[#0e1b2a]" />
) : (
<ChevronRight className="w-4 h-4 text-[#0e1b2a]" />
)}
<span
className={`font-medium text-sm capitalize ${
hasSelected ? 'text-[#112868]' : 'text-[#0e1b2a]'
}`}
>
{resource.replace(/_/g, ' ')}
</span>
{hasSelected && (
<span className="text-xs text-[#112868] bg-[rgba(17,40,104,0.1)] px-2 py-0.5 rounded">
Selected
<button
type="button"
onClick={() => toggleResource(resource)}
className="w-full flex items-center justify-between p-3 hover:bg-[rgba(0,0,0,0.02)] transition-colors"
>
<div className="flex items-center gap-2">
{isExpanded ? (
<ChevronDown className="w-4 h-4 text-[#0e1b2a]" />
) : (
<ChevronRight className="w-4 h-4 text-[#0e1b2a]" />
)}
<span
className={`font-medium text-sm capitalize ${
hasSelected
? "text-[#112868]"
: "text-[#0e1b2a]"
}`}
>
{resource.replace(/_/g, " ")}
</span>
)}
</div>
</button>
{isExpanded && (
<div className="px-3 pb-3 pt-2 border-t border-[rgba(0,0,0,0.05)]">
<div className="flex flex-wrap gap-4">
{Array.from(actions).map((action) => {
const isChecked = selectedPermissions.some((p) => {
// Check for exact match
if (p.resource === resource && p.action === action) return true;
// Check for wildcard resource with exact action
if (p.resource === '*' && p.action === action) return true;
// Check for exact resource with wildcard action
if (p.resource === resource && p.action === '*') return true;
// Check for wildcard resource with wildcard action
if (p.resource === '*' && p.action === '*') return true;
return false;
});
return (
<label
key={`${resource}-${action}`}
className="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
checked={isChecked}
onChange={(e) => handlePermissionChange(resource, action, e.target.checked)}
className="w-4 h-4 text-[#112868] border-[rgba(0,0,0,0.2)] rounded focus:ring-[#112868]/20"
/>
<span className="text-sm text-[#0e1b2a] capitalize">{action}</span>
</label>
);
})}
{hasSelected && (
<span className="text-xs text-[#112868] bg-[rgba(17,40,104,0.1)] px-2 py-0.5 rounded">
Selected
</span>
)}
</div>
</div>
)}
</div>
);
})}
</button>
{isExpanded && (
<div className="px-3 pb-3 pt-2 border-t border-[rgba(0,0,0,0.05)]">
<div className="flex flex-wrap gap-4">
{Array.from(actions).map((action) => {
const isChecked = selectedPermissions.some(
(p) => {
// Check for exact match
if (
p.resource === resource &&
p.action === action
)
return true;
// Check for wildcard resource with exact action
if (
p.resource === "*" &&
p.action === action
)
return true;
// Check for exact resource with wildcard action
if (
p.resource === resource &&
p.action === "*"
)
return true;
// Check for wildcard resource with wildcard action
if (p.resource === "*" && p.action === "*")
return true;
return false;
},
);
return (
<label
key={`${resource}-${action}`}
className="flex items-center gap-2 cursor-pointer"
>
<input
type="checkbox"
checked={isChecked}
onChange={(e) =>
handlePermissionChange(
resource,
action,
e.target.checked,
)
}
className="w-4 h-4 text-[#112868] border-[rgba(0,0,0,0.2)] rounded focus:ring-[#112868]/20"
/>
<span className="text-sm text-[#0e1b2a] capitalize">
{action}
</span>
</label>
);
})}
</div>
</div>
)}
</div>
);
},
)}
</div>
)}
</div>

View File

@ -15,6 +15,8 @@ import {
import { roleService } from "@/services/role-service";
import { departmentService } from "@/services/department-service";
import { designationService } from "@/services/designation-service";
import { moduleService } from "@/services/module-service";
import { useAppSelector } from "@/hooks/redux-hooks";
// Validation schema
const newUserSchema = z
@ -36,6 +38,7 @@ const newUserSchema = z
role_ids: z.array(z.string()).min(1, "At least one role is required"),
department_id: z.string().optional(),
designation_id: z.string().optional(),
module_ids: z.array(z.string()).optional(),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
@ -80,6 +83,7 @@ export const NewUserModal = ({
role_ids: [],
department_id: "",
designation_id: "",
module_ids: [],
},
});
@ -87,6 +91,10 @@ export const NewUserModal = ({
const roleIdsValue = watch("role_ids");
const departmentIdValue = watch("department_id");
const designationIdValue = watch("designation_id");
const moduleIdsValue = watch("module_ids");
const roles = useAppSelector((state) => state.auth.roles);
const isSuperAdmin = roles.includes("super_admin");
// Reset form when modal closes
useEffect(() => {
@ -102,6 +110,7 @@ export const NewUserModal = ({
role_ids: [],
department_id: "",
designation_id: "",
module_ids: [],
});
clearErrors();
}
@ -159,6 +168,18 @@ export const NewUserModal = ({
};
};
const loadModules = async (page: number, limit: number) => {
const tenantId = isSuperAdmin ? defaultTenantId : undefined;
const response = await moduleService.getAvailable(page, limit, tenantId);
return {
options: response.data.map((module) => ({
value: module.id,
label: module.name,
})),
pagination: response.pagination,
};
};
const handleFormSubmit = async (data: NewUserFormData): Promise<void> => {
clearErrors();
try {
@ -180,7 +201,8 @@ export const NewUserModal = ({
detail.path === "last_name" ||
detail.path === "status" ||
detail.path === "auth_provider" ||
detail.path === "role_ids"
detail.path === "role_ids" ||
detail.path === "module_ids"
) {
setError(detail.path as keyof NewUserFormData, {
type: "server",
@ -346,6 +368,17 @@ export const NewUserModal = ({
error={errors.status?.message}
/>
</div>
<div className="pb-4">
<MultiselectPaginatedSelect
label="Assign Modules"
placeholder="Select Modules"
value={moduleIdsValue || []}
onValueChange={(value) => setValue("module_ids", value)}
onLoadOptions={loadModules}
error={errors.module_ids?.message}
/>
</div>
</div>
</form>
</Modal>

View File

@ -387,7 +387,7 @@ export const WorkflowDefinitionModal = ({
const payload: any = {
...data,
source_module: selectedModuleNames,
tenant_id: tenantId || undefined,
tenantId: tenantId || undefined,
};
// In edit mode, if steps or transitions are not edited (hidden in UI)

View File

@ -24,12 +24,12 @@ interface WorkflowDefinitionsTableProps {
}
const WorkflowDefinitionsTable = ({
tenantId: propsTenantId,
tenantId: tenantId,
compact = false,
showHeader = true,
}: WorkflowDefinitionsTableProps): ReactElement => {
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId);
const effectiveTenantId = propsTenantId || reduxTenantId || undefined;
const effectiveTenantId = tenantId || reduxTenantId || undefined;
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
@ -232,11 +232,11 @@ const WorkflowDefinitionsTable = ({
),
},
{
key: "updated_at",
label: "Last Updated",
key: "created_at",
label: "Created Date",
render: (wf) => (
<span className="text-sm text-[#6b7280]">
{formatDate(wf.updated_at)}
{formatDate(wf.created_at)}
</span>
),
},
@ -288,6 +288,16 @@ const WorkflowDefinitionsTable = ({
<Power className="w-4 h-4" />
</button>
)}
{wf.status === "deprecated" && (
<button
onClick={() => handleActivate(wf.id)}
disabled={isActionLoading}
className="p-1 hover:bg-green-50 rounded-md transition-colors text-green-600"
title="Activate"
>
<Play className="w-4 h-4" />
</button>
)}
<button
onClick={() => {

View File

@ -39,6 +39,7 @@ interface RolesTableProps {
}
export const RolesTable = ({ tenantId, showHeader = true, compact = false }: RolesTableProps): ReactElement => {
const [roles, setRoles] = useState<Role[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);

View File

@ -6,7 +6,7 @@ import {
NewUserModal,
ViewUserModal,
EditUserModal,
DeleteConfirmationModal,
// DeleteConfirmationModal,
DataTable,
Pagination,
FilterDropdown,
@ -84,11 +84,11 @@ export const UsersTable = ({
// View, Edit, Delete modals
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
// const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [selectedUserName, setSelectedUserName] = useState<string>("");
// const [selectedUserName, setSelectedUserName] = useState<string>("");
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const [isDeleting, setIsDeleting] = useState<boolean>(false);
// const [isDeleting, setIsDeleting] = useState<boolean>(false);
const fetchUsers = async (
page: number,
@ -162,9 +162,12 @@ export const UsersTable = ({
};
// Edit user handler
const handleEditUser = (userId: string, userName: string): void => {
const handleEditUser = (
userId: string,
// , userName: string
): void => {
setSelectedUserId(userId);
setSelectedUserName(userName);
// setSelectedUserName(userName);
setEditModalOpen(true);
};
@ -193,7 +196,7 @@ export const UsersTable = ({
showToast.success(message, description);
setEditModalOpen(false);
setSelectedUserId(null);
setSelectedUserName("");
// setSelectedUserName("");
await fetchUsers(currentPage, limit, statusFilter, orderBy);
} catch (err: any) {
throw err;
@ -203,29 +206,32 @@ export const UsersTable = ({
};
// Delete user handler
const handleDeleteUser = (userId: string, userName: string): void => {
const handleDeleteUser = (
userId: string,
// , userName: string
): void => {
setSelectedUserId(userId);
setSelectedUserName(userName);
setDeleteModalOpen(true);
// setSelectedUserName(userName);
// setDeleteModalOpen(true);
};
// Confirm delete handler
const handleConfirmDelete = async (): Promise<void> => {
if (!selectedUserId) return;
// const handleConfirmDelete = async (): Promise<void> => {
// if (!selectedUserId) return;
try {
setIsDeleting(true);
await userService.delete(selectedUserId);
setDeleteModalOpen(false);
setSelectedUserId(null);
setSelectedUserName("");
await fetchUsers(currentPage, limit, statusFilter, orderBy);
} catch (err: any) {
throw err; // Let the modal handle the error display
} finally {
setIsDeleting(false);
}
};
// try {
// setIsDeleting(true);
// await userService.delete(selectedUserId);
// setDeleteModalOpen(false);
// setSelectedUserId(null);
// setSelectedUserName("");
// await fetchUsers(currentPage, limit, statusFilter, orderBy);
// } catch (err: any) {
// throw err; // Let the modal handle the error display
// } finally {
// setIsDeleting(false);
// }
// };
// Load user for view/edit
const loadUser = async (id: string): Promise<User> => {
@ -336,11 +342,14 @@ export const UsersTable = ({
<ActionDropdown
onView={() => handleViewUser(user.id)}
onEdit={() =>
handleEditUser(user.id, `${user.first_name} ${user.last_name}`)
}
onDelete={() =>
handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)
handleEditUser(
user.id,
// , `${user.first_name} ${user.last_name}`
)
}
// onDelete={() =>
// handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)
// }
/>
</div>
),
@ -369,10 +378,16 @@ export const UsersTable = ({
<ActionDropdown
onView={() => handleViewUser(user.id)}
onEdit={() =>
handleEditUser(user.id, `${user.first_name} ${user.last_name}`)
handleEditUser(
user.id,
// , `${user.first_name} ${user.last_name}`
)
}
onDelete={() =>
handleDeleteUser(user.id, `${user.first_name} ${user.last_name}`)
handleDeleteUser(
user.id,
// `${user.first_name} ${user.last_name}`
)
}
/>
</div>
@ -496,7 +511,7 @@ export const UsersTable = ({
onClose={() => {
setEditModalOpen(false);
setSelectedUserId(null);
setSelectedUserName("");
// setSelectedUserName("");
}}
userId={selectedUserId}
onLoadUser={loadUser}
@ -505,7 +520,7 @@ export const UsersTable = ({
defaultTenantId={tenantId || undefined}
/>
<DeleteConfirmationModal
{/* <DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
@ -517,7 +532,7 @@ export const UsersTable = ({
message="Are you sure you want to delete this user"
itemName={selectedUserName}
isLoading={isDeleting}
/>
/> */}
</>
);
}
@ -656,7 +671,7 @@ export const UsersTable = ({
onClose={() => {
setEditModalOpen(false);
setSelectedUserId(null);
setSelectedUserName("");
// setSelectedUserName("");
}}
userId={selectedUserId}
onLoadUser={loadUser}
@ -664,7 +679,7 @@ export const UsersTable = ({
isLoading={isUpdating}
/>
<DeleteConfirmationModal
{/* <DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
@ -676,7 +691,7 @@ export const UsersTable = ({
message="Are you sure you want to delete this user"
itemName={selectedUserName}
isLoading={isDeleting}
/>
/> */}
</>
);
};

View File

@ -1,20 +1,20 @@
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import type { ReactElement } from 'react';
import { Shield, ArrowLeft } from 'lucide-react';
import { authService } from '@/services/auth-service';
import { FormField } from '@/components/shared';
import { PrimaryButton } from '@/components/shared';
import { useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import type { ReactElement } from "react";
import { Shield, ArrowLeft } from "lucide-react";
import { authService } from "@/services/auth-service";
import { FormField } from "@/components/shared";
import { PrimaryButton } from "@/components/shared";
// Zod validation schema
const forgotPasswordSchema = z.object({
email: z
.string()
.min(1, 'Email is required')
.email('Please enter a valid email address'),
.min(1, "Email is required")
.email("Please enter a valid email address"),
});
type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>;
@ -22,8 +22,8 @@ type ForgotPasswordFormData = z.infer<typeof forgotPasswordSchema>;
const ForgotPassword = (): ReactElement => {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [error, setError] = useState<string>('');
const [success, setSuccess] = useState<string>('');
const [error, setError] = useState<string>("");
const [success, setSuccess] = useState<string>("");
const {
register,
@ -32,30 +32,37 @@ const ForgotPassword = (): ReactElement => {
clearErrors,
} = useForm<ForgotPasswordFormData>({
resolver: zodResolver(forgotPasswordSchema),
mode: 'onBlur',
mode: "onBlur",
});
const onSubmit = async (data: ForgotPasswordFormData): Promise<void> => {
setError('');
setSuccess('');
setError("");
setSuccess("");
clearErrors();
setIsLoading(true);
try {
const response = await authService.forgotPassword({ email: data.email });
if (response.success && response.data) {
setSuccess(response.data.message || 'If an account exists with this email, a password reset link has been sent.');
setSuccess(
response.data.message ||
"If an account exists with this email, a password reset link has been sent.",
);
}
} catch (err: any) {
console.error('Forgot password error:', err);
console.error("Forgot password error:", err);
if (err?.response?.data?.error?.message) {
setError(err.response.data.error.message);
} else if (err?.response?.data?.error) {
setError(typeof err.response.data.error === 'string' ? err.response.data.error : 'Failed to send reset email');
setError(
typeof err.response.data.error === "string"
? err.response.data.error
: "Failed to send reset email",
);
} else if (err?.message) {
setError(err.message);
} else {
setError('Failed to send reset email. Please try again.');
setError("Failed to send reset email. Please try again.");
}
} finally {
setIsLoading(false);
@ -83,7 +90,8 @@ const ForgotPassword = (): ReactElement => {
Forgot Password
</h1>
<p className="text-sm md:text-base text-[#6b7280]">
Enter your email address and we'll send you a link to reset your password
Enter your email address and we'll send you a link to reset your
password
</p>
</div>
@ -118,7 +126,7 @@ const ForgotPassword = (): ReactElement => {
placeholder="Enter your email"
required
error={errors.email?.message}
{...register('email')}
{...register("email")}
/>
{/* Submit Button */}
@ -129,7 +137,7 @@ const ForgotPassword = (): ReactElement => {
disabled={isLoading}
className="w-full h-12 text-sm font-medium"
>
{isLoading ? 'Sending...' : 'Send Reset Link'}
{isLoading ? "Sending..." : "Send Reset Link"}
</PrimaryButton>
</div>
</form>
@ -139,7 +147,7 @@ const ForgotPassword = (): ReactElement => {
<PrimaryButton
type="button"
size="large"
onClick={() => navigate('/')}
onClick={() => navigate("/")}
className="w-full h-12 text-sm font-medium"
>
Back to Login

View File

@ -888,6 +888,7 @@ const SettingsTab = ({ tenant: _tenant }: SettingsTabProps): ReactElement => {
Selected: {logoFile.name}
</div>
)}
{/* <img src={tenant.logo} alt="" /> */}
</div>
{/* Favicon */}

View File

@ -8,7 +8,7 @@ import {
NewUserModal,
ViewUserModal,
EditUserModal,
DeleteConfirmationModal,
// DeleteConfirmationModal,
DataTable,
Pagination,
FilterDropdown,
@ -56,7 +56,9 @@ const getStatusVariant = (
};
const Users = (): ReactElement => {
const { canCreate, canUpdate, canDelete } = usePermissions();
const { canCreate, canUpdate
// , canDelete
} = usePermissions();
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
@ -87,11 +89,11 @@ const Users = (): ReactElement => {
// View, Edit, Delete modals
const [viewModalOpen, setViewModalOpen] = useState<boolean>(false);
const [editModalOpen, setEditModalOpen] = useState<boolean>(false);
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
// const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
const [selectedUserName, setSelectedUserName] = useState<string>("");
// const [selectedUserName, setSelectedUserName] = useState<string>("");
const [isUpdating, setIsUpdating] = useState<boolean>(false);
const [isDeleting, setIsDeleting] = useState<boolean>(false);
// const [isDeleting, setIsDeleting] = useState<boolean>(false);
const fetchUsers = async (
page: number,
@ -160,9 +162,11 @@ const Users = (): ReactElement => {
};
// Edit user handler
const handleEditUser = (userId: string, userName: string): void => {
const handleEditUser = (userId: string
// , userName: string
): void => {
setSelectedUserId(userId);
setSelectedUserName(userName);
// setSelectedUserName(userName);
setEditModalOpen(true);
};
@ -199,29 +203,29 @@ const Users = (): ReactElement => {
};
// Delete user handler
const handleDeleteUser = (userId: string, userName: string): void => {
setSelectedUserId(userId);
setSelectedUserName(userName);
setDeleteModalOpen(true);
};
// const handleDeleteUser = (userId: string, userName: string): void => {
// setSelectedUserId(userId);
// setSelectedUserName(userName);
// setDeleteModalOpen(true);
// };
// Confirm delete handler
const handleConfirmDelete = async (): Promise<void> => {
if (!selectedUserId) return;
// const handleConfirmDelete = async (): Promise<void> => {
// if (!selectedUserId) return;
try {
setIsDeleting(true);
await userService.delete(selectedUserId);
setDeleteModalOpen(false);
setSelectedUserId(null);
setSelectedUserName("");
await fetchUsers(currentPage, limit, statusFilter, orderBy);
} catch (err: any) {
throw err;
} finally {
setIsDeleting(false);
}
};
// try {
// setIsDeleting(true);
// await userService.delete(selectedUserId);
// setDeleteModalOpen(false);
// setSelectedUserId(null);
// setSelectedUserName("");
// await fetchUsers(currentPage, limit, statusFilter, orderBy);
// } catch (err: any) {
// throw err;
// } finally {
// setIsDeleting(false);
// }
// };
// Load user for view/edit
const loadUser = async (id: string): Promise<User> => {
@ -318,19 +322,19 @@ const Users = (): ReactElement => {
? () =>
handleEditUser(
user.id,
`${user.first_name} ${user.last_name}`,
)
: undefined
}
onDelete={
canDelete("users")
? () =>
handleDeleteUser(
user.id,
`${user.first_name} ${user.last_name}`,
// `${user.first_name} ${user.last_name}`,
)
: undefined
}
// onDelete={
// canDelete("users")
// ? () =>
// handleDeleteUser(
// user.id,
// `${user.first_name} ${user.last_name}`,
// )
// : undefined
// }
/>
</div>
),
@ -363,19 +367,19 @@ const Users = (): ReactElement => {
? () =>
handleEditUser(
user.id,
`${user.first_name} ${user.last_name}`,
)
: undefined
}
onDelete={
canDelete("users")
? () =>
handleDeleteUser(
user.id,
`${user.first_name} ${user.last_name}`,
// `${user.first_name} ${user.last_name}`,
)
: undefined
}
// onDelete={
// canDelete("users")
// ? () =>
// handleDeleteUser(
// user.id,
// `${user.first_name} ${user.last_name}`,
// )
// : undefined
// }
/>
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
@ -544,7 +548,7 @@ const Users = (): ReactElement => {
onClose={() => {
setEditModalOpen(false);
setSelectedUserId(null);
setSelectedUserName("");
// setSelectedUserName("");
}}
userId={selectedUserId}
onLoadUser={loadUser}
@ -553,7 +557,7 @@ const Users = (): ReactElement => {
/>
{/* Delete Confirmation Modal */}
<DeleteConfirmationModal
{/* <DeleteConfirmationModal
isOpen={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
@ -565,7 +569,7 @@ const Users = (): ReactElement => {
message="Are you sure you want to delete this user"
itemName={selectedUserName}
isLoading={isDeleting}
/>
/> */}
</Layout>
);
};

View File

@ -33,7 +33,7 @@ export interface LoginResponse {
data: {
user: User;
tenant_id: string;
tenant: Tenant;
// tenant: Tenant;
roles: string[];
permissions: Permission[];
access_token: string;

View File

@ -6,7 +6,7 @@ import type {
GetUserResponse,
UpdateUserRequest,
UpdateUserResponse,
DeleteUserResponse,
// DeleteUserResponse,
} from '@/types/user';
const getAllUsers = async (
@ -57,8 +57,8 @@ export const userService = {
const response = await apiClient.put<UpdateUserResponse>(`/users/${id}`, data);
return response.data;
},
delete: async (id: string): Promise<DeleteUserResponse> => {
const response = await apiClient.delete<DeleteUserResponse>(`/users/${id}`);
return response.data;
},
// delete: async (id: string): Promise<DeleteUserResponse> => {
// const response = await apiClient.delete<DeleteUserResponse>(`/users/${id}`);
// return response.data;
// },
};

View File

@ -22,9 +22,9 @@ class WorkflowService {
search?: string;
}): Promise<WorkflowDefinitionsResponse> {
const queryParams: any = { ...params };
if (params?.tenantId) {
queryParams.tenant_id = params.tenantId;
}
// if (params?.tenantId) {
// queryParams.tenant_id = params.tenantId;
// }
const response = await apiClient.get<WorkflowDefinitionsResponse>(`${this.baseUrl}/definitions`, { params: queryParams });
return response.data;
}
@ -36,37 +36,37 @@ class WorkflowService {
}
async createDefinition(data: CreateWorkflowDefinitionData, tenantId?: string): Promise<WorkflowDefinitionResponse> {
const params = tenantId ? { tenant_id: tenantId } : {};
const params = tenantId ? { tenantId: tenantId } : {};
const response = await apiClient.post<WorkflowDefinitionResponse>(`${this.baseUrl}/definitions`, data, { params });
return response.data;
}
async updateDefinition(id: string, data: UpdateWorkflowDefinitionData, tenantId?: string): Promise<WorkflowDefinitionResponse> {
const params = tenantId ? { tenant_id: tenantId } : {};
const params = tenantId ? { tenantId: tenantId } : {};
const response = await apiClient.put<WorkflowDefinitionResponse>(`${this.baseUrl}/definitions/${id}`, data, { params });
return response.data;
}
async deleteDefinition(id: string, tenantId?: string): Promise<WorkflowDeleteResponse> {
const params = tenantId ? { tenant_id: tenantId } : {};
const params = tenantId ? { tenantId: tenantId } : {};
const response = await apiClient.delete<WorkflowDeleteResponse>(`${this.baseUrl}/definitions/${id}`, { params });
return response.data;
}
async cloneDefinition(id: string, name: string, tenantId?: string): Promise<WorkflowDefinitionResponse> {
const params = tenantId ? { tenant_id: tenantId } : {};
const params = tenantId ? { tenantId: tenantId } : {};
const response = await apiClient.post<WorkflowDefinitionResponse>(`${this.baseUrl}/definitions/${id}/clone`, { name }, { params });
return response.data;
}
async activateDefinition(id: string, tenantId?: string): Promise<WorkflowDefinitionResponse> {
const params = tenantId ? { tenant_id: tenantId } : {};
const params = tenantId ? { tenantId: tenantId } : {};
const response = await apiClient.post<WorkflowDefinitionResponse>(`${this.baseUrl}/definitions/${id}/activate`, {}, { params });
return response.data;
}
async deprecateDefinition(id: string, tenantId?: string): Promise<WorkflowDefinitionResponse> {
const params = tenantId ? { tenant_id: tenantId } : {};
const params = tenantId ? { tenantId: tenantId } : {};
const response = await apiClient.post<WorkflowDefinitionResponse>(`${this.baseUrl}/definitions/${id}/deprecate`, {}, { params });
return response.data;
}

View File

@ -1,5 +1,5 @@
import { createSlice, createAsyncThunk, type PayloadAction } from '@reduxjs/toolkit';
import { authService, type LoginRequest, type LoginResponse, type LoginError, type GeneralError, type Permission, type Tenant } from '@/services/auth-service';
import { authService, type LoginRequest, type LoginResponse, type LoginError, type GeneralError, type Permission } from '@/services/auth-service';
interface User {
id: string;
@ -11,7 +11,7 @@ interface User {
interface AuthState {
user: User | null;
tenantId: string | null;
tenant: Tenant | null;
// tenant: Tenant | null;
roles: string[];
permissions: Permission[];
accessToken: string | null;
@ -27,7 +27,7 @@ interface AuthState {
const initialState: AuthState = {
user: null,
tenantId: null,
tenant: null,
// tenant: null,
roles: [],
permissions: [],
accessToken: null,
@ -85,7 +85,7 @@ const authSlice = createSlice({
logout: (state) => {
state.user = null;
state.tenantId = null;
state.tenant = null;
// state.tenant = null;
state.roles = [];
state.permissions = [];
state.accessToken = null;
@ -110,7 +110,7 @@ const authSlice = createSlice({
state.isLoading = false;
state.user = action.payload.data.user;
state.tenantId = action.payload.data.tenant_id;
state.tenant = action.payload.data.tenant;
// state.tenant = action.payload.data.tenant;
state.roles = action.payload.data.roles;
state.permissions = action.payload.data.permissions || [];
state.accessToken = action.payload.data.access_token;
@ -144,7 +144,7 @@ const authSlice = createSlice({
// Reset to initial state
state.user = null;
state.tenantId = null;
state.tenant = null;
// state.tenant = null;
state.roles = [];
state.permissions = [];
state.accessToken = null;
@ -160,7 +160,7 @@ const authSlice = createSlice({
// Even if API call fails, clear local state
state.user = null;
state.tenantId = null;
state.tenant = null;
// state.tenant = null;
state.roles = [];
state.permissions = [];
state.accessToken = null;

View File

@ -29,6 +29,10 @@ export interface User {
id: string;
name: string;
};
modules?: {
id: string;
name: string;
}[];
created_at: string;
updated_at: string;
}
@ -59,6 +63,7 @@ export interface CreateUserRequest {
role_ids?: string[];
department_id?: string;
designation_id?: string;
module_ids?: string[];
}
export interface CreateUserResponse {
@ -83,6 +88,7 @@ export interface UpdateUserRequest {
role_ids?: string[];
department_id?: string;
designation_id?: string;
module_ids?: string[];
}
export interface UpdateUserResponse {

View File

@ -68,7 +68,7 @@ export interface CreateWorkflowDefinitionData {
source_module_id?: string[];
steps: Partial<WorkflowStep>[];
transitions: Partial<WorkflowTransition>[];
tenant_id?: string;
tenantId?: string;
}
export interface UpdateWorkflowDefinitionData {
@ -80,7 +80,7 @@ export interface UpdateWorkflowDefinitionData {
source_module_id?: string[];
steps?: Partial<WorkflowStep>[];
transitions?: Partial<WorkflowTransition>[];
tenant_id?: string;
tenantId?: string;
}
export interface WorkflowDefinitionsResponse {