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

View File

@ -16,6 +16,8 @@ import {
import { roleService } from "@/services/role-service"; import { roleService } from "@/services/role-service";
import { departmentService } from "@/services/department-service"; import { departmentService } from "@/services/department-service";
import { designationService } from "@/services/designation-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"; import type { User } from "@/types/user";
// Validation schema // Validation schema
@ -30,6 +32,7 @@ const editUserSchema = z.object({
role_ids: z.array(z.string()).min(1, "At least one role is required"), role_ids: z.array(z.string()).min(1, "At least one role is required"),
department_id: z.string().optional(), department_id: z.string().optional(),
designation_id: z.string().optional(), designation_id: z.string().optional(),
module_ids: z.array(z.string()).optional(),
}); });
type EditUserFormData = z.infer<typeof editUserSchema>; type EditUserFormData = z.infer<typeof editUserSchema>;
@ -80,6 +83,11 @@ export const EditUserModal = ({
const roleIdsValue = watch("role_ids"); const roleIdsValue = watch("role_ids");
const departmentIdValue = watch("department_id"); const departmentIdValue = watch("department_id");
const designationIdValue = watch("designation_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< const [initialRoleOptions, setInitialRoleOptions] = useState<
{ value: string; label: string }[] { value: string; label: string }[]
@ -92,6 +100,9 @@ export const EditUserModal = ({
value: string; value: string;
label: string; label: string;
} | null>(null); } | null>(null);
const [initialModuleOptions, setInitialModuleOptions] = useState<
{ value: string; label: string }[]
>([]);
// Load roles for dropdown - ensure selected role is included // Load roles for dropdown - ensure selected role is included
const loadRoles = async (page: number, limit: number) => { 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 // Load user data when modal opens
useEffect(() => { useEffect(() => {
if (isOpen && userId) { if (isOpen && userId) {
@ -200,8 +223,15 @@ export const EditUserModal = ({
role_ids: roleIds, role_ids: roleIds,
department_id: departmentId, department_id: departmentId,
designation_id: designationId, 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) { if (defaultTenantId) {
setValue("tenant_id", defaultTenantId, { shouldValidate: true }); setValue("tenant_id", defaultTenantId, { shouldValidate: true });
} }
@ -230,7 +260,9 @@ export const EditUserModal = ({
role_ids: [], role_ids: [],
department_id: "", department_id: "",
designation_id: "", designation_id: "",
module_ids: [],
}); });
setInitialModuleOptions([]);
setLoadError(null); setLoadError(null);
clearErrors(); clearErrors();
} }
@ -412,6 +444,18 @@ export const EditUserModal = ({
error={errors.status?.message} error={errors.status?.message}
/> />
</div> </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> </div>
)} )}
</form> </form>

View File

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

View File

@ -15,6 +15,8 @@ import {
import { roleService } from "@/services/role-service"; import { roleService } from "@/services/role-service";
import { departmentService } from "@/services/department-service"; import { departmentService } from "@/services/department-service";
import { designationService } from "@/services/designation-service"; import { designationService } from "@/services/designation-service";
import { moduleService } from "@/services/module-service";
import { useAppSelector } from "@/hooks/redux-hooks";
// Validation schema // Validation schema
const newUserSchema = z const newUserSchema = z
@ -36,6 +38,7 @@ const newUserSchema = z
role_ids: z.array(z.string()).min(1, "At least one role is required"), role_ids: z.array(z.string()).min(1, "At least one role is required"),
department_id: z.string().optional(), department_id: z.string().optional(),
designation_id: z.string().optional(), designation_id: z.string().optional(),
module_ids: z.array(z.string()).optional(),
}) })
.refine((data) => data.password === data.confirmPassword, { .refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match", message: "Passwords don't match",
@ -80,6 +83,7 @@ export const NewUserModal = ({
role_ids: [], role_ids: [],
department_id: "", department_id: "",
designation_id: "", designation_id: "",
module_ids: [],
}, },
}); });
@ -87,6 +91,10 @@ export const NewUserModal = ({
const roleIdsValue = watch("role_ids"); const roleIdsValue = watch("role_ids");
const departmentIdValue = watch("department_id"); const departmentIdValue = watch("department_id");
const designationIdValue = watch("designation_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 // Reset form when modal closes
useEffect(() => { useEffect(() => {
@ -102,6 +110,7 @@ export const NewUserModal = ({
role_ids: [], role_ids: [],
department_id: "", department_id: "",
designation_id: "", designation_id: "",
module_ids: [],
}); });
clearErrors(); 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> => { const handleFormSubmit = async (data: NewUserFormData): Promise<void> => {
clearErrors(); clearErrors();
try { try {
@ -180,7 +201,8 @@ export const NewUserModal = ({
detail.path === "last_name" || detail.path === "last_name" ||
detail.path === "status" || detail.path === "status" ||
detail.path === "auth_provider" || detail.path === "auth_provider" ||
detail.path === "role_ids" detail.path === "role_ids" ||
detail.path === "module_ids"
) { ) {
setError(detail.path as keyof NewUserFormData, { setError(detail.path as keyof NewUserFormData, {
type: "server", type: "server",
@ -346,6 +368,17 @@ export const NewUserModal = ({
error={errors.status?.message} error={errors.status?.message}
/> />
</div> </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> </div>
</form> </form>
</Modal> </Modal>

View File

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

View File

@ -24,12 +24,12 @@ interface WorkflowDefinitionsTableProps {
} }
const WorkflowDefinitionsTable = ({ const WorkflowDefinitionsTable = ({
tenantId: propsTenantId, tenantId: tenantId,
compact = false, compact = false,
showHeader = true, showHeader = true,
}: WorkflowDefinitionsTableProps): ReactElement => { }: WorkflowDefinitionsTableProps): ReactElement => {
const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId); const reduxTenantId = useSelector((state: RootState) => state.auth.tenantId);
const effectiveTenantId = propsTenantId || reduxTenantId || undefined; const effectiveTenantId = tenantId || reduxTenantId || undefined;
const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]); const [definitions, setDefinitions] = useState<WorkflowDefinition[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true); const [isLoading, setIsLoading] = useState<boolean>(true);
@ -232,11 +232,11 @@ const WorkflowDefinitionsTable = ({
), ),
}, },
{ {
key: "updated_at", key: "created_at",
label: "Last Updated", label: "Created Date",
render: (wf) => ( render: (wf) => (
<span className="text-sm text-[#6b7280]"> <span className="text-sm text-[#6b7280]">
{formatDate(wf.updated_at)} {formatDate(wf.created_at)}
</span> </span>
), ),
}, },
@ -288,6 +288,16 @@ const WorkflowDefinitionsTable = ({
<Power className="w-4 h-4" /> <Power className="w-4 h-4" />
</button> </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 <button
onClick={() => { onClick={() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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