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 { 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,20 +324,27 @@ 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;
|
||||||
@ -401,44 +354,56 @@ export const EditRoleModal = ({
|
|||||||
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 &&
|
||||||
|
"message" in errorObj
|
||||||
|
? errorObj.message
|
||||||
|
: null) ||
|
||||||
|
(typeof errorObj === "string" ? errorObj : null) ||
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
error?.message ||
|
error?.message ||
|
||||||
'Failed to update role. Please try again.';
|
"Failed to update role. Please try again.";
|
||||||
setError('root', {
|
setError("root", {
|
||||||
type: 'server',
|
type: "server",
|
||||||
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to update role. Please try again.',
|
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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 &&
|
||||||
|
"message" in errorObj
|
||||||
|
? errorObj.message
|
||||||
|
: null) ||
|
||||||
|
(typeof errorObj === "string" ? errorObj : null) ||
|
||||||
error?.response?.data?.message ||
|
error?.response?.data?.message ||
|
||||||
error?.message ||
|
error?.message ||
|
||||||
'Failed to create role. Please try again.';
|
"Failed to create role. Please try again.";
|
||||||
setError('root', {
|
setError("root", {
|
||||||
type: 'server',
|
type: "server",
|
||||||
message: typeof errorMessage === 'string' ? errorMessage : 'Failed to create role. Please try again.',
|
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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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={() => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}
|
||||||
/>
|
/> */}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
},
|
// },
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user