feat: Remove user deletion functionality, add module association to user types, and refactor tenant object out of auth state.
This commit is contained in:
parent
1025504a55
commit
5a0da32699
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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={() => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
/> */}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -888,6 +888,7 @@ const SettingsTab = ({ tenant: _tenant }: SettingsTabProps): ReactElement => {
|
||||
Selected: {logoFile.name}
|
||||
</div>
|
||||
)}
|
||||
{/* <img src={tenant.logo} alt="" /> */}
|
||||
</div>
|
||||
|
||||
{/* Favicon */}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -33,7 +33,7 @@ export interface LoginResponse {
|
||||
data: {
|
||||
user: User;
|
||||
tenant_id: string;
|
||||
tenant: Tenant;
|
||||
// tenant: Tenant;
|
||||
roles: string[];
|
||||
permissions: Permission[];
|
||||
access_token: string;
|
||||
|
||||
@ -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;
|
||||
// },
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user