Module Overview
The Users Module in the Nimbly Audit Admin system is a comprehensive user management solution that handles all aspects of user lifecycle management including creation, retrieval, updating, and deletion of user accounts. The module is built using React, Redux for state management, and follows a component-based architecture pattern.
Key Features
- User Management: Complete CRUD operations for managing auditors and other user roles
- Role-Based Access Control: Granular permission system controlling access to different features
- Off-Days Management: System for managing auditor non-operational days
- Bulk Operations: Support for bulk user operations including downloads and updates
- Analytics Integration: Comprehensive user analytics and reporting capabilities
- Multi-platform Support: Responsive design supporting both desktop and mobile views
Module Structure
The Users module is organized into several key directories:
src/
├── components/auditors/ # Core auditor components
├── pages/ # Page-level components
├── routes/ # Routing configuration
├── services/ # API services
├── reducers/ # Redux state management
├── sagas/ # Redux-Saga side effects
└── constants/ # Constants and configurations
User List Module
Overview
The User List module provides a comprehensive interface for viewing and managing all users in the system. It supports advanced features like sorting, filtering, pagination, and bulk operations.
Architecture
The User List module follows a container-presenter pattern with clear separation of concerns:
graph TD A[AuditorsPage Component] --> B[AuditorManager Component] B --> C[AuditorList Container] C --> D[AuditorListTable Desktop View] C --> E[AuditorListCard Mobile View] B --> F[AuditorListHeader] F --> G[Search Component] F --> H[Filter Components] F --> I[Bulk Action Components] J[Redux Store] --> K[Auditors Reducer] J --> L[Users Reducer] J --> M[Permissions Reducer] N[API Services] --> O[User Service] N --> P[Auditor Service] K --> C L --> C M --> C O --> K P --> K
Implementation Details
1. Main Components
AuditorsPage Component (src/pages/auditors.js:1-40)
The main page component that serves as the entry point for the auditors module. It manages the overall layout and modal states:
class AuditorsPage extends React.Component {
render() {
const { modalVisible, bulkModal } = this.props;
return (
<React.Fragment>
{modalVisible ? (
<Suspense fallback={<div />}>
<AuditorEditor />
</Suspense>
) : null}
{bulkModal ? (
<Suspense fallback={<div />}>
<AuditorBulkModal />
</Suspense>
) : null}
<Layout>
<Suspense fallback={<div />}>
<AuditorManager />
</Suspense>
</Layout>
</React.Fragment>
);
}
}Key features:
- Lazy loading of components for performance optimization
- Modal management for editor and bulk operations
- Integration with Redux for state management
AuditorListContainer Component (src/components/auditors/AuditorList/AuditorListContainer.tsx:1-204)
The container component that manages the presentation logic and data flow:
const AuditorListContainer = (props: AuditorListContainerProps) => {
const { auditorProcessedUsers, auditorIndex, displayedItem, totalUsers, isLoading, setPageIndex, pageIndex } = props;
const slicedProcessedUsers = cloneDeep(auditorProcessedUsers).splice(auditorIndex, displayedItem);
return (
<Root>
<AuditorListTable {...tableProps} />
<AuditorListCard {...cardProps} />
<PaginationContainer>
{!isLoading && Object.keys(props.users).length > 0 ? (
<Pagination
page="auditor"
currentIndex={pageIndex}
totalItem={totalUsers!}
listPerPage={props.itemListLimit}
onChange={(index: number) => setPageIndex(index)}
/>
) : null}
</PaginationContainer>
</Root>
);
};Key responsibilities:
- Data slicing for pagination
- Responsive view management (desktop vs mobile)
- Pagination control
AuditorListTable Component (src/components/auditors/AuditorList/AuditorListTable.tsx:1-431)
The table component for desktop view with comprehensive user display:
const AuditorListTable = (props: AuditorListTableProps) => {
const { t } = useTranslation();
const history = useHistory();
const [selectedRow, setSelectedRow] = useState(null);
// Sorting functionality
const handleSort = (column: string) => {
props.sortType(column);
};
// Row click handler
const handleRowClick = (user: any, role: string) => {
setSelectedRow(user);
history.push({
pathname: `/admin/auditors/${user}`,
});
};
// Table rendering with columns:
// - Checkbox (for bulk operations)
// - Avatar
// - Email/Username
// - Display Name
// - Phone Number
// - Role
// - Status
// - WhatsApp Number
// - WhatsApp Notification Type
// - Actions (Edit/Block)
};2. State Management
The User List module uses Redux for state management with the following structure:
Auditors Reducer (src/reducers/auditors/auditors.reducer.ts)
const initialState = {
users: {},
totalUsers: null,
isLoading: false,
error: null,
modalVisible: false,
bulkModal: false,
selectedUsers: {},
filters: {
status: 'all',
role: 'all',
search: '',
sortBy: 'email',
sortDirection: 'asc'
},
pagination: {
pageIndex: 0,
itemsPerPage: 20
}
};Actions and Action Types (src/reducers/auditors/auditors.actions.ts)
Key actions include:
FETCH_AUDITORS_REQUEST: Initiate user fetchFETCH_AUDITORS_SUCCESS: Store fetched usersFETCH_AUDITORS_FAILURE: Handle fetch errorsUPDATE_AUDITOR_FILTER: Update filter criteriaSELECT_AUDITOR_FOR_BULK: Select users for bulk operationsCLEAR_AUDITOR_SELECTION: Clear bulk selections
3. API Integration
User Service (src/services/user.api.ts)
The service layer handles all HTTP requests for user operations:
export const userService = {
// Fetch all users with pagination and filters
getUsers: async (params: UserQueryParams) => {
const { page, limit, sortBy, sortDirection, filter, search } = params;
const response = await axios.get('/api/v1/users', {
params: {
page,
limit,
sortBy,
sortDirection,
...filter,
search
}
});
return response.data;
},
// Get single user details
getUser: async (userId: string) => {
const response = await axios.get(`/api/v1/users/${userId}`);
return response.data;
},
// Update user status (block/unblock)
updateUserStatus: async (userId: string, status: string) => {
const response = await axios.put(`/api/v1/users/${userId}/status`, { status });
return response.data;
}
};4. Features and Functionality
Sorting
The user list supports sorting by multiple columns:
- Email/Username
- Display Name
- Role
- Status
Sorting is bidirectional (ascending/descending) and maintains state across pagination.
Filtering
Users can be filtered by:
- Status: All, Active, Disabled, Fresh
- Role: All roles, Admin, Supervisor, Auditor, etc.
- Search: Real-time search across email, name, and phone number
Pagination
- Server-side pagination for performance
- Configurable items per page (10, 20, 50, 100)
- Page navigation with first, previous, next, last controls
- Current page and total page display
Bulk Operations
- Select all/individual users
- Bulk download user data
- Bulk status updates
- Bulk role changes
Row Actions
Each user row provides:
- Edit: Navigate to user edit page (role-based access)
- Block/Unblock: Toggle user status (role-based access)
- Click to View: Row click navigates to user details
5. Permissions and Access Control
The User List module implements comprehensive role-based access control:
// Access permissions structure
const access = {
view: hasPermission(RoleResources.ADMIN_USER_ALL),
create: hasPermission(RoleResources.ADMIN_USER_CREATE),
edit: hasPermission(RoleResources.ADMIN_USER_EDIT),
delete: hasPermission(RoleResources.ADMIN_USER_DELETE)
};Permission checks are performed at:
- Component mount (redirect if no access)
- Action buttons (disable/hide based on permissions)
- API calls (server-side validation)
6. Performance Optimizations
- Lazy Loading: Components are loaded on demand using React.lazy
- Virtualization: Large lists use windowing for efficient rendering
- Memoization: Expensive computations are memoized using React.memo and useMemo
- Debounced Search: Search input is debounced to reduce API calls
- Optimistic Updates: UI updates before server confirmation for better UX
7. Mobile Responsiveness
The User List module provides a dedicated mobile view:
AuditorListCard Component (src/components/auditors/AuditorList/AuditorListCard.tsx)
- Card-based layout for mobile devices
- Swipe actions for edit/block operations
- Simplified information display
- Touch-optimized controls
Data Flow
sequenceDiagram participant U as User participant C as Component participant R as Redux Store participant S as Saga participant A as API U->>C: Navigate to Users Page C->>R: Dispatch FETCH_AUDITORS_REQUEST R->>S: Trigger fetchAuditorsSaga S->>A: GET /api/v1/users A-->>S: Return users data S->>R: Dispatch FETCH_AUDITORS_SUCCESS R-->>C: Update component props C-->>U: Render user list U->>C: Click sort column C->>R: Dispatch UPDATE_AUDITOR_FILTER R->>S: Trigger fetchAuditorsSaga S->>A: GET /api/v1/users (with sort params) A-->>S: Return sorted data S->>R: Dispatch FETCH_AUDITORS_SUCCESS R-->>C: Update component props C-->>U: Render sorted list
Error Handling
The User List module implements comprehensive error handling:
- Network Errors: Retry mechanisms with exponential backoff
- Validation Errors: Client-side validation before API calls
- Permission Errors: Graceful degradation of features
- Data Errors: Fallback UI for missing or corrupt data
- User Feedback: Toast notifications for all operations
Testing
The module includes comprehensive test coverage:
- Unit Tests: Component logic and utility functions
- Integration Tests: Component interactions and Redux flow
- E2E Tests: User workflows and scenarios
- Performance Tests: Load testing for large datasets
User Details Module
Overview
The User Details module provides comprehensive functionality for viewing and editing individual user information. It consists of two main implementations: one for modal-based editing (AuditorEditor) and one for full-page editing (auditorEdit). Both share similar functionality but differ in their presentation and navigation patterns.
Architecture
The User Details module follows a sophisticated component hierarchy with clear separation between presentation and business logic:
graph TD A[Route: /admin/auditors/:userId] --> B[auditorEdit Page Component] A2[Modal Trigger] --> C[AuditorEditor Modal Component] B --> D[AuditorEditContainer] C --> E[AuditorEditorContainer] D --> F[AuditorEditForm] E --> G[AuditorEditorForm] F --> H[Form Fields Components] G --> H I[Redux Store] --> J[User State] I --> K[Departments State] I --> L[Permissions State] M[API Services] --> N[fetchSingleUser] M --> O[updateSingleUser] M --> P[createNewUser] Q[Validation Services] --> R[Phone Validation] Q --> S[Email Validation] Q --> T[Password Validation]
Implementation Details
1. Page-Based User Details (auditorEdit.tsx)
The page-based implementation is used when navigating directly to a user’s details page:
Route: /admin/auditors/:userId or /admin/auditors/new
Key Components:
auditorEdit Component (src/pages/auditorEdit.tsx:1-787)
This is the main container component that manages the entire user editing flow:
export const AuditorEdit = (props: any) => {
const params = useParams<{ userId: string }>();
const [form, setForm] = useState<AuditorEditorFormFields>({
role: '',
email: '',
displayName: '',
phoneNumber: '',
departments: [],
whatsappNumber: '',
whatsappAdvanceNotification: false,
siteAuditors: [],
siteSupervisors: [],
password: '',
});
// Fetch user data on mount
useEffect(() => {
if (!location.pathname.includes('new')) {
fetchUser();
}
}, [params.userId]);
const fetchUser = async () => {
const data = await fetchSingleUser(params.userId);
// Transform and set form data
setForm({
role: data.role,
email: data.email,
displayName: data.displayName || '',
phoneNumber: String(data?.phoneNumber || ''),
departments: department || [],
whatsappNumber: data?.whatsappNumber,
whatsappAdvanceNotification: data?.whatsappAdvanceNotification,
siteAuditors: siteAuditors || [],
siteSupervisors: siteSupervisors || [],
password: data.password,
});
};
}Key features of this component:
- Dynamic Loading: Determines whether to create a new user or edit existing based on route
- Comprehensive Form State: Manages all user fields including advanced permissions
- Site Assignment: Handles supervisor and auditor assignments to specific sites
- Department Management: Manages user department associations
- Password Generation: Includes secure password generation based on organization rules
2. Form Structure and Fields
AuditorEditForm Component (src/pages/AuditorEditForm.tsx:1-806)
The form component provides a comprehensive UI for user data entry:
const AuditorEditorForm = (props: AuditorEditorFormProps) => {
// Core user fields
return (
<Wrapper>
<GridContainer>
<FormColumn>
{/* User ID/Email Field with availability check */}
<FormInput
name="email"
type="email"
placeholder={t('placeholder.auditorPage.email')}
value={props.form.email}
hasError={props.formErrors.email}
disabled={!!props.isBusy || !!emailInputDisabled}
onChange={props.handleInputChange}
/>
{/* Password field with generation */}
<FormInput
name="password"
type="text"
placeholder={t('placeholder.auditorPage.password')}
value={props.form.password}
disabled={!canEditPassword}
/>
<FilledButton onClick={generatePasswordBasedOnRule}>
Generate
</FilledButton>
{/* Role selection with permissions */}
<SingleSelect
value={roleValue}
options={selectOptions()}
onChange={props.onSelectRole}
isDisabled={!props.canEditRole}
isOptionDisabled={selectOptionDisabled}
/>
{/* Department multi-select */}
<FilterField
options={departmentOptions}
onChange={props.handleSelectDepartment}
value={mapValuesToOptions(props.form.departments, departmentMap)}
/>
{/* WhatsApp notification settings */}
<RadioContainer>
<Radio onClick={() => props.handleWhatsappNotificationType(false)}>
Basic
</Radio>
<Radio onClick={() => props.handleWhatsappNotificationType(true)}>
Advanced
</Radio>
</RadioContainer>
</FormColumn>
{/* Site assignments section */}
<div>
{/* Supervisor assignments */}
<div className="supervisor-section">
<FilterField options={siteOptions} />
<SingleSelect options={departmentOptions} />
<EmailContainer>
{/* List of assigned sites */}
</EmailContainer>
</div>
{/* Auditor assignments */}
<div className="auditor-section">
{/* Similar structure for auditor assignments */}
</div>
</div>
</GridContainer>
</Wrapper>
);
};3. Validation Logic
The module implements comprehensive validation for all user fields:
Email Validation:
- Checks format validity
- Ensures uniqueness (except for current user)
- Supports both email and username formats
Phone Number Validation:
- Uses
libphonenumber-jsfor international format validation - Validates both primary and WhatsApp numbers
- Supports country-specific formats
Password Validation:
- Enforces organization-specific password rules
- Minimum length requirements
- Character complexity requirements
- Generated passwords follow security best practices
Role Validation:
- Ensures user cannot assign roles above their access level
- Validates role permissions based on editor’s role
4. Advanced Features
Site and Department Assignment
The module supports complex organizational structures through site and department assignments:
const handleAddSupervisor = (siteID: string[], deptID: string) => {
const siteSupervisorsArr: { siteID: string; departmentID: string }[] = [];
siteID.forEach((siteID) => {
const alreadyPresent = siteSupervisors.some(
(supervisor) => supervisor.siteID === siteID && supervisor.departmentID === deptID
);
if (!alreadyPresent) {
siteSupervisorsArr.push({ siteID: siteID, departmentID: deptID });
}
});
const updatedSiteSupervisors = [...siteSupervisors, ...siteSupervisorsArr];
setForm({ ...form, siteSupervisors: updatedSiteSupervisors });
};This allows:
- Multiple site assignments per user
- Department-specific permissions within sites
- Hierarchical permission structures
- Bulk assignment operations
Username Availability Check
For new users, the system provides real-time username availability checking:
const handleCheckAvailability = async () => {
setLoading(true);
const isAvailable = await checkUserNameAvaibility(form.email);
setEmailAvailable(isAvailable);
setMessage(isAvailable ? 'Available' : 'Already Taken');
setLoading(false);
};Password Generation
The system includes intelligent password generation based on organization rules:
const generatePasswordBasedOnRule = () => {
const passwordSettingRule = useSelector(
(state: RootState) => state?.organization?.organization?.passwordConfiguration
);
const newPassword = generatedPassword(passwordSettingRule);
props.handleInputValue('password', newPassword);
props.setIsPasswordGenerated(true);
};5. State Management
The User Details module integrates with Redux for state management:
Redux State Structure:
{
admin: {
manage: {
upsertedUser_loading: boolean,
upsertedUser_error: string | null,
upsertedUser_userID: string | null
}
},
users: {
paginateUsers: { [userId: string]: User }
},
departments: {
departments: { [deptId: string]: Department }
},
settings: {
userRolesMap: { [role: string]: RoleLabel }
}
}Key Actions:
createUserAsync: Creates new user with validationupdateUserAsync: Updates existing user datafetchSingleUser: Retrieves individual user detailsclearUserState: Resets form state after operations
6. API Integration
The module uses several API endpoints for user operations:
Fetch User Details:
const fetchSingleUser = async (userId: string) => {
const response = await axios.get(`/api/v1/users/${userId}`);
return response.data;
};Create User:
const createNewUser = async (userData: UserData) => {
const response = await axios.post('/api/v1/users', {
email: userData.email,
displayName: userData.displayName,
phoneNumber: userData.phoneNumber,
role: userData.role,
departmentIDs: userData.departmentIDs,
whatsappNumber: userData.whatsappNumber,
whatsappAdvanceNotification: userData.whatsappAdvanceNotification,
siteSupervisors: userData.siteSupervisors,
siteAuditors: userData.siteAuditors,
password: userData.password
});
return response.data;
};Update User:
const updateSingleUser = async (userData: UpdateUserData) => {
const response = await axios.put(`/api/v1/users/${userData.targetEmail}`, {
email: userData.email,
phoneNumber: userData.phoneNumber,
displayName: userData.displayName,
role: userData.role,
whatsappNumber: userData.whatsappNumber,
whatsappAdvanceNotification: userData.whatsappAdvanceNotification,
siteSupervisors: userData.siteSupervisors,
siteAuditors: userData.siteAuditors,
departmentIDs: userData.departmentIDs,
password: userData.password
});
return response.data;
};7. Permission-Based Access Control
The module implements granular permission controls:
// Permission checks
const canEditUser = state.userAccess.admin.user.all.permissions.edit;
const canEditRole = state.userAccess.admin.user.role.permissions.edit;
const canCreateUser = state.userAccess.admin.user.all.permissions.create;
// Field-level permissions
const emailInputDisabled = props.isBusy || (props.selectedUserKey && isActiveUser) && isUpdatingUser;
const roleSelectDisabled = !props.canEditRole || props.selectedUserKey === props.editorUserKey;
const passwordEditDisabled = loggedInUserRole !== 'account_holder' && loggedInUserRole !== 'superadmin';8. Error Handling and User Feedback
The module provides comprehensive error handling:
useEffect(() => {
if (!upsertedUser_loading) {
if (upsertedUser_error) {
toast.error(upsertedUser_error);
setIsUploadingSingle(false);
props.clearUserState();
setIsBusy(false);
} else if (upsertedUser_userID) {
toast.success(isCreate ? `${email} successfully added.` : `Successfully updated user`);
history.goBack();
}
}
}, [upsertedUser_loading, upsertedUser_error]);Data Flow
sequenceDiagram participant U as User participant C as Component participant V as Validator participant R as Redux participant A as API U->>C: Navigate to user details C->>A: fetchSingleUser(userId) A-->>C: Return user data C->>C: Populate form fields U->>C: Edit user information C->>V: Validate fields V-->>C: Return validation results U->>C: Submit form C->>V: Final validation V-->>C: Validation passed C->>R: Dispatch updateUserAsync R->>A: PUT /api/v1/users/:id A-->>R: Return updated user R-->>C: Update success C-->>U: Show success message
Mobile Responsiveness
The User Details module is fully responsive:
- Grid Layout: Switches from 2-column to 1-column on mobile
- Touch-Optimized Controls: Larger touch targets for mobile
- Simplified Navigation: Back button instead of modal close
- Adaptive Form Fields: Full-width fields on mobile devices
Performance Optimizations
- Lazy Loading: Components loaded on demand
- Debounced Validation: Reduces validation calls during typing
- Memoized Selectors: Prevents unnecessary re-renders
- Optimistic Updates: UI updates before server confirmation
User Creation Module
Overview
The User Creation module provides multiple methods for adding new users to the system: single user creation through forms and bulk user creation through CSV import. The module ensures data integrity through comprehensive validation and provides real-time feedback during the creation process.
Architecture
graph TD A[User Creation Methods] --> B[Single User Creation] A --> C[Bulk User Creation] B --> D[Form Input] B --> E[Validation] B --> F[API Call] C --> G[CSV Upload] C --> H[File Parsing] C --> I[Batch Processing] J[Validation Services] --> K[Email Validation] J --> L[Phone Validation] J --> M[Role Validation] J --> N[Username Availability] O[Firebase Functions] --> P[createNewAuditor] O --> Q[Role Assignment] R[Redux Actions] --> S[createUserAsync] R --> T[fetchPaginateUsers] R --> U[clearUserState]
Implementation Details
1. Single User Creation
Single user creation is handled through the same form components used for editing, but with specific logic for new users:
Route: /admin/auditors/new
Key Implementation (src/pages/auditorEdit.tsx:298-404):
const handleCreateUser = async () => {
// Permission check
if (!props.canCreateUser) {
toast.error('You are not allowed to create new users');
return;
}
setIsBusy(true);
setIsUploadingSingle(true);
const hasError = validateForm();
// Handle email/username formatting
let emailOrUsername = form.email;
if (form.email.includes('@') && form.email.includes('.')) {
emailOrUsername = form.email.toLowerCase();
}
if (!hasError) {
const data = {
email: emailOrUsername,
displayName: form.displayName.trim().replace(/\s+/g, ' '),
phoneNumber: form.phoneNumber?.trim()?.replace(/ /g, ''),
role: form.role,
whatsappNumber: form.whatsappNumber?.replace(/\s/g, ''),
whatsappAdvanceNotification: form.whatsappAdvanceNotification,
siteSupervisors: form?.siteSupervisors || [],
siteAuditors: form?.siteAuditors || [],
departmentIDs: form?.departments || [],
password: form?.password,
};
const result = await createNewUser(data);
if (result.message === 'FAILED') {
toast.error(result.error);
setIsBusy(false);
} else {
toast.success('User created successfully');
history.goBack();
}
}
};Key Features:
-
Username/Email Flexibility:
- Supports both email addresses and usernames
- Automatically lowercases email addresses
- Validates format based on input type
-
Real-time Availability Check:
const handleCheckAvailability = async () => { setLoading(true); const isAvailable = await checkUserNameAvaibility(form.email); setEmailAvailable(isAvailable); setMessage(isAvailable ? 'Available' : 'Already Taken'); setLoading(false); }; -
Password Generation:
- Generates secure passwords based on organization rules
- Provides copy-to-clipboard functionality
- Shows generated password for user communication
-
Department Assignment:
- Multiple department selection
- Visual tags for selected departments
- Easy removal of assignments
-
Site Assignments:
- Separate supervisor and auditor site assignments
- Department-specific permissions per site
- Bulk assignment capabilities
2. Bulk User Creation
The bulk user creation feature allows administrators to import multiple users via CSV file upload:
Implementation (src/pages/auditorEdit.tsx:466-594):
const handleFileChange = (event: any) => {
// Permission check
if (!props.canCreateUser) {
toast.error('You are not allowed to create new users');
return;
}
const fileTypes = ['csv'];
if (event.target.files.length && event.target.files.length === 1) {
const file = event.target.files[0];
const extension = file.name.split('.').pop().toLowerCase();
const isSuccess = fileTypes.indexOf(extension) > -1;
if (isSuccess) {
Papa.parse(file, {
skipEmptyLines: true,
complete: (results) => handleCreateUsers(results),
});
} else {
toast.error('Cannot parse the file. Please download the template and try again.');
}
}
};CSV Format Requirements:
The CSV file must follow this format:
Email Address,Full Name,Role,Phone Number (e.g. +628123456789)
john.doe@example.com,John Doe,Auditor,+628123456789
jane.smith@example.com,Jane Smith,Supervisor,+628123456790Bulk Processing Logic:
const handleCreateUsers = async (results: any) => {
const data = results.data;
// Validate CSV headers
if (
data[0][0] === 'Email Address' &&
data[0][1] === 'Full Name' &&
data[0][2] === 'Role' &&
data[0][3] === 'Phone Number (e.g. +628123456789)'
) {
setIsUploadingBulk(true);
setIsBusy(true);
setUploadProgress(0);
// Filter out existing emails and invalid phone numbers
const emails = Object.keys(props.users).map((uid) => props.users[uid].email);
const entries = data.slice(1).filter((entry: string[]) => {
const index = emails.indexOf(entry[0]);
const phoneNumber = parsePhoneNumberFromString(entry[3] || '');
return index === -1 && phoneNumber && phoneNumber.isValid();
});
// Process in batches of 5
const recursiveAddNewAuditor = async (e: any[]): Promise<any> => {
if (e.length === 0) return [];
const entriesToProcess = e.splice(0, 5);
const users = entriesToProcess.map(async (entry: any) => {
const userData = {
email: entry[0].toLowerCase(),
displayName: entry[1],
phoneNumber: entry[3]?.trim()?.replace(/ /g, '') || '',
};
// Create user and assign role
const res = await createNewAuditor(userData);
const newUserID = res.data.uid;
// Assign role via API
await fetch(`${apiURL}/user-roles/user-to-role`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
authorization: authToken,
},
body: JSON.stringify({
userID: newUserID,
role: newUserRole,
}),
});
});
return merged.concat(await recursiveAddNewAuditor(e));
};
}
};Key Features of Bulk Creation:
- Batch Processing: Processes users in batches of 5 to avoid overwhelming the server
- Progress Tracking: Real-time progress bar showing upload status
- Validation:
- Validates email uniqueness
- Validates phone number format
- Validates role assignments
- Error Handling: Tracks successes and failures separately
- Role Mapping: Maps CSV role names to system role IDs
3. Validation Rules
The User Creation module implements comprehensive validation:
Email/Username Validation:
const validateEmail = (email: string) => {
// Check if it's an email format
if (email.includes('@') && email.includes('.')) {
return isValidEmail(email);
}
// Username validation (2-8 characters, no spaces)
return /^[a-zA-Z0-9]{2,8}$/.test(email);
};Phone Number Validation:
const validatePhoneNumber = (phoneNumber: string) => {
if (!phoneNumber) return false;
const parsed = parsePhoneNumberFromString(phoneNumber);
if (typeof parsed === 'undefined') return true;
return parsed.isValid();
};Password Requirements:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- Optional special characters based on organization settings
Role Assignment Rules:
- Users can only assign roles below their access level
- Superadmin and account_holder roles cannot be assigned via UI
- Role permissions are validated server-side
4. API Integration
Create Single User:
POST /api/v1/users
{
"email": "user@example.com",
"displayName": "User Name",
"phoneNumber": "+628123456789",
"role": "auditor",
"departmentIDs": ["dept1", "dept2"],
"whatsappNumber": "+628123456789",
"whatsappAdvanceNotification": false,
"siteSupervisors": [
{ "siteID": "site1", "departmentID": "dept1" }
],
"siteAuditors": [
{ "siteID": "site2", "departmentID": "dept2" }
],
"password": "SecurePassword123!"
}Firebase Function for Bulk Creation:
const createNewAuditor = firebase.functions().httpsCallable('createNewAuditor');
const result = await createNewAuditor({
email: userData.email,
displayName: userData.displayName,
phoneNumber: userData.phoneNumber
});5. Error Handling
The module provides comprehensive error handling for various scenarios:
-
Duplicate Email/Username:
if (uniqueEmail.length !== 0) { toast.error('Email is already registered'); return; } -
Invalid Phone Number:
if (validatePhoneNumber(form.phoneNumber)) { setFormErrors({ ...formErrors, phoneNumber: true }); toast.error('Invalid phone number format'); } -
Permission Denied:
if (!props.canCreateUser) { toast.error('You are not allowed to create new users'); return; } -
Network Errors:
try { const result = await createNewUser(data); } catch (error) { toast.error('Network error. Please try again.'); setIsBusy(false); }
6. Success Handling
Upon successful user creation:
-
Single User:
- Success toast notification
- Navigation back to user list
- Automatic refresh of user list
- Clear form state
-
Bulk Users:
- Summary of successful/failed imports
- Detailed error report for failures
- Automatic refresh of user list
- Progress reset
Security Considerations
-
Input Sanitization:
- Email addresses are lowercased and trimmed
- Display names remove extra spaces
- Phone numbers remove formatting characters
-
Permission Checks:
- Client-side permission validation
- Server-side permission enforcement
- Role-based creation limits
-
Password Security:
- Generated passwords meet complexity requirements
- Passwords are never displayed in logs
- Secure transmission over HTTPS
-
Rate Limiting:
- Bulk operations limited to 5 concurrent requests
- Username availability checks are debounced
- CSV file size limits enforced
Best Practices
-
User Communication:
- Always communicate generated passwords securely
- Provide clear error messages
- Show progress for long operations
-
Data Validation:
- Validate all inputs client-side first
- Server-side validation as final check
- Provide specific validation error messages
-
Bulk Import:
- Provide downloadable CSV template
- Validate file format before processing
- Handle partial failures gracefully
-
Performance:
- Batch API calls for bulk operations
- Show progress indicators
- Implement proper error recovery
User Update Module
Overview
The User Update module handles modifications to existing user data, including profile information, role changes, department assignments, and status updates. It provides both individual and bulk update capabilities with comprehensive validation and permission checks.
Architecture
graph TD A[User Update Operations] --> B[Individual Updates] A --> C[Bulk Updates] A --> D[Status Updates] B --> E[Profile Updates] B --> F[Role Changes] B --> G[Department Changes] B --> H[Site Assignments] C --> I[Bulk Status Change] C --> J[Bulk Role Update] C --> K[Bulk Department Assignment] L[Validation Layer] --> M[Permission Validation] L --> N[Data Validation] L --> O[Conflict Resolution] P[Update Strategies] --> Q[Optimistic Updates] P --> R[Transactional Updates] P --> S[Partial Updates]
Implementation Details
1. Individual User Updates
The primary user update functionality is implemented in the auditorEdit component:
Implementation (src/pages/auditorEdit.tsx:409-464):
const handleUpdateUser = async () => {
// Permission validation
if (!props.canEditUser) {
toast.error('You are not allowed to edit users');
return;
}
// Email uniqueness validation
const uniqueEmail = Object.keys(props.users || {})
?.filter((key) => props?.users?.[key]?.email === form?.email) || [];
if (form.email !== currentTargetEmail) {
if (uniqueEmail.length !== 0) {
toast.error('Email is already registered');
return;
}
}
setIsBusy(true);
setIsPasswordGenerated(false);
setIsGenerated(false);
const data = {
targetEmail: currentTargetEmail,
email: form.email,
phoneNumber: form.phoneNumber,
displayName: form.displayName,
role: form.role,
whatsappNumber: form.whatsappNumber?.replace(/\s/g, ''),
whatsappAdvanceNotification: form.whatsappAdvanceNotification,
siteSupervisors: form?.siteSupervisors || [],
siteAuditors: form?.siteAuditors || [],
departmentIDs: form?.departments || [],
password: form.password,
};
const result = await updateSingleUser(data);
if (result.message === 'FAILED') {
toast.error(result.error);
} else {
toast.success('User updated successfully');
await fetchUser(); // Refresh user data
}
setIsBusy(false);
};2. Update Types and Validation
Profile Information Updates
Profile updates include changes to:
- Display name
- Phone numbers (primary and WhatsApp)
- Email/username (with special validation)
- WhatsApp notification preferences
Email Update Validation:
// Special handling for email updates
if (form.email !== currentTargetEmail) {
// Check if new email is already in use
const emailExists = await checkEmailAvailability(form.email);
if (!emailExists) {
toast.error('Email is already registered');
return;
}
// Additional validation for email format
if (!isValidEmail(form.email) && form.email.includes('@')) {
toast.error('Invalid email format');
return;
}
}Role Updates
Role updates have special permission requirements:
const canUpdateRole = () => {
// User cannot change their own role
if (props.selectedUserKey === props.editorUserKey) {
return false;
}
// User cannot assign roles above their level
const targetRoleLevel = props.roleOptionsMap[form.role]?.level;
const editorAccessLevel = props.accessLevel;
return targetRoleLevel < editorAccessLevel && props.canEditRole;
};Department and Site Assignment Updates
Complex organizational structure updates:
// Add supervisor to sites
const handleAddSupervisor = (siteID: string[], deptID: string) => {
const siteSupervisorsArr: { siteID: string; departmentID: string }[] = [];
siteID.forEach((siteID) => {
const alreadyPresent = siteSupervisors.some(
(supervisor) => supervisor.siteID === siteID &&
supervisor.departmentID === deptID
);
if (!alreadyPresent) {
siteSupervisorsArr.push({ siteID: siteID, departmentID: deptID });
}
});
const updatedSiteSupervisors = [...siteSupervisors, ...siteSupervisorsArr];
setForm({ ...form, siteSupervisors: updatedSiteSupervisors });
};
// Remove supervisor from site
const handleRemoveSupervisor = (siteID: string, deptID: string) => {
const { siteSupervisors = [] } = form;
const filteredSiteSupervisor = siteSupervisors.filter(
(supervisor) => !(supervisor.siteID === siteID &&
supervisor.departmentID === deptID)
);
setForm({ ...form, siteSupervisors: filteredSiteSupervisor });
};3. Status Updates (Block/Unblock)
User status updates are handled through the list interface:
Implementation (src/components/auditors/AuditorList/AuditorListTable.tsx:85-130):
const renderBlockButton = (user: any, block: string, unblock: string) => {
const blockButton = (
<img
alt={block}
title={block}
src={TrashIcon}
onClick={(e) => {
e.stopPropagation();
if (props.isNotBlockable(props.users[user])) {
return null;
} else {
props.handleBlockUser(user);
}
}}
style={{ opacity: props.isNotBlockable(props.users[user]) ? 0.4 : 1 }}
/>
);
const unblockButton = (
<Button
small
inversed
disabled={props.isNotBlockable(props.users[user])}
onClick={(e) => {
e.stopPropagation();
props.handleBlockUser(user);
}}
>
{unblock}
</Button>
);
// Show appropriate button based on current status
if (props.access.delete) {
if (props.users[user].status !== 'disabled') {
return blockButton;
}
return unblockButton;
}
return null;
};4. Bulk Update Operations
The system supports bulk updates through the AuditorBulkModal component:
Bulk Modal Implementation (src/components/auditors/AuditorBulkModal/AuditorBulkModal.tsx):
const AuditorBulkModal = () => {
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [updateType, setUpdateType] = useState<'status' | 'role' | 'department'>('status');
const [updateValue, setUpdateValue] = useState<any>(null);
const handleBulkUpdate = async () => {
const updates = selectedUsers.map(userId => ({
userId,
updates: {
[updateType]: updateValue
}
}));
try {
const results = await Promise.all(
updates.map(update => updateUser(update.userId, update.updates))
);
const successCount = results.filter(r => r.success).length;
toast.success(`Successfully updated ${successCount} users`);
if (results.some(r => !r.success)) {
const failCount = results.filter(r => !r.success).length;
toast.error(`Failed to update ${failCount} users`);
}
} catch (error) {
toast.error('Bulk update failed');
}
};
};5. Password Updates
Password updates have special security considerations:
const handlePasswordUpdate = async () => {
// Only certain roles can update passwords
const canUpdatePassword =
loggedInUserRole === 'account_holder' ||
loggedInUserRole === 'superadmin' ||
location.pathname.includes('new');
if (!canUpdatePassword) {
toast.error('You do not have permission to update passwords');
return;
}
// Validate password complexity
if (!validatePasswordComplexity(form.password)) {
toast.error('Password does not meet complexity requirements');
return;
}
// Update password
const result = await updateUserPassword(userId, form.password);
if (result.success) {
toast.success('Password updated successfully');
setIsPasswordGenerated(false);
}
};6. API Integration
Update User Endpoint:
PUT /api/v1/users/:userId
{
"email": "updated@example.com",
"displayName": "Updated Name",
"phoneNumber": "+628123456789",
"role": "supervisor",
"departmentIDs": ["dept1", "dept2"],
"whatsappNumber": "+628123456789",
"whatsappAdvanceNotification": true,
"siteSupervisors": [
{ "siteID": "site1", "departmentID": "dept1" }
],
"siteAuditors": [
{ "siteID": "site2", "departmentID": "dept2" }
]
}Update User Status:
PUT /api/v1/users/:userId/status
{
"status": "disabled" | "active"
}Bulk Update Endpoint:
POST /api/v1/users/bulk-update
{
"userIds": ["user1", "user2", "user3"],
"updates": {
"role": "auditor",
"departmentIDs": ["dept1"]
}
}7. Optimistic Updates
The system implements optimistic updates for better user experience:
const handleOptimisticUpdate = async (userId: string, updates: any) => {
// Update UI immediately
dispatch(updateUserOptimistically(userId, updates));
try {
// Make API call
const result = await updateUser(userId, updates);
if (!result.success) {
// Revert on failure
dispatch(revertUserUpdate(userId));
toast.error('Update failed');
}
} catch (error) {
// Revert on error
dispatch(revertUserUpdate(userId));
toast.error('Network error');
}
};8. Conflict Resolution
The system handles update conflicts gracefully:
const handleUpdateConflict = async (conflict: UpdateConflict) => {
switch (conflict.type) {
case 'EMAIL_EXISTS':
toast.error('Email is already in use by another user');
break;
case 'STALE_DATA':
// Refresh data and retry
const freshData = await fetchUser(conflict.userId);
const retry = await updateUser(conflict.userId, {
...conflict.updates,
version: freshData.version
});
break;
case 'PERMISSION_CHANGED':
toast.error('Your permissions have changed. Please refresh the page.');
break;
default:
toast.error('Update failed due to conflict');
}
};Update Validation Rules
-
Email Updates:
- Must be unique across the system
- Format validation for email addresses
- Username format validation (2-8 characters)
-
Role Updates:
- Cannot assign roles above editor’s level
- Cannot change own role
- Special roles (superadmin, account_holder) restricted
-
Phone Number Updates:
- International format validation
- Country code requirements
- WhatsApp number separate validation
-
Department Updates:
- Must be valid department IDs
- Circular dependency checks
- Permission validation per department
-
Site Assignment Updates:
- Site must exist and be active
- Department must be valid for site
- No duplicate assignments
Security Considerations
-
Permission Checks:
// Hierarchical permission validation const validateUpdatePermission = (editor: User, target: User, updates: any) => { // Cannot edit users at or above your level if (target.role.level >= editor.role.level) { return false; } // Cannot assign roles above your level if (updates.role && updates.role.level >= editor.role.level) { return false; } // Field-specific permissions if (updates.password && !editor.permissions.updatePassword) { return false; } return true; }; -
Audit Trail:
- All updates logged with timestamp
- Editor information recorded
- Previous values preserved
- Change reasons tracked
-
Transactional Updates:
- Complex updates wrapped in transactions
- Rollback on partial failure
- Consistency guarantees
Best Practices
-
Update Strategies:
- Use optimistic updates for better UX
- Batch related updates together
- Validate before submission
- Handle conflicts gracefully
-
User Communication:
- Clear success/error messages
- Explain validation failures
- Confirm destructive actions
- Show update progress
-
Performance:
- Debounce rapid updates
- Use field-level updates when possible
- Cache validation results
- Minimize API calls
-
Data Integrity:
- Validate all inputs
- Check permissions server-side
- Maintain referential integrity
- Handle race conditions
User Delete Module
Overview
The User Delete module handles the removal of users from the system. Due to the critical nature of user deletion, the module implements multiple safeguards including soft deletes (status changes), permission checks, and special utilities for customer success teams to handle bulk deletions across organizations.
Architecture
graph TD A[User Delete Operations] --> B[Soft Delete/Block] A --> C[Hard Delete] A --> D[Bulk Delete] B --> E[Status Change to Disabled] B --> F[Preserve User Data] B --> G[Audit Trail] C --> H[CS Utility Only] C --> I[Permanent Removal] C --> J[Data Cleanup] K[Permission Checks] --> L[Role-Based Access] K --> M[Hierarchical Validation] K --> N[Self-Delete Prevention] O[Safety Measures] --> P[Confirmation Dialogs] O --> Q[Audit Logging] O --> R[Rollback Capability]
Implementation Details
1. Soft Delete (User Blocking)
The primary method for “deleting” users is through soft delete, which changes the user’s status to ‘disabled’:
Implementation in User List (src/components/auditors/AuditorList/AuditorListTable.tsx:85-130):
const handleBlockUser = async (userId: string) => {
// Permission check
if (!props.access.delete) {
toast.error('You do not have permission to block users');
return;
}
// Prevent self-blocking
if (userId === currentUserId) {
toast.error('You cannot block your own account');
return;
}
// Hierarchical permission check
const targetUser = props.users[userId];
if (isNotBlockable(targetUser)) {
toast.error('You cannot block users at or above your access level');
return;
}
// Confirm action
const confirmed = await confirmDialog({
title: 'Block User',
message: `Are you sure you want to block ${targetUser.displayName}?`,
confirmText: 'Block',
cancelText: 'Cancel'
});
if (confirmed) {
try {
await updateUserStatus(userId, 'disabled');
toast.success('User blocked successfully');
props.fetchPaginateUsers({ limit: props.itemListLimit });
} catch (error) {
toast.error('Failed to block user');
}
}
};Key Features of Soft Delete:
- Reversible: Users can be unblocked/reactivated
- Data Preservation: All user data remains intact
- Access Revocation: Blocked users cannot log in
- Audit Trail: All blocks/unblocks are logged
Permission Validation:
const isNotBlockable = (user: User) => {
// Cannot block users with higher or equal access level
const userAccessLevel = props.roleOptionsMap[user.role]?.level || 0;
const currentAccessLevel = props.accessLevel;
// Special protection for system roles
if (user.role === 'superadmin' || user.role === 'account_holder') {
return true;
}
return userAccessLevel >= currentAccessLevel;
};2. Hard Delete (Customer Success Utility)
Hard deletion is restricted to customer success teams and involves permanent removal of user data:
Delete Users Component (src/components/customerSuccessUtility/deleteUsers/DeleteUsers.tsx:1-294):
export const DeleteUsers = (props: any) => {
const [orgList, setOrgList] = useState<any[]>([]);
const [auditorList, setAuditorList] = useState<any>([]);
const [selectedOrg, setSelectedOrg] = useState('');
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [isDeleteUserLoading, setDeleteUserLoading] = useState(false);
const handleSubmit = async () => {
setDeleteUserLoading(true);
// Multi-level confirmation for hard delete
const confirmed = await multiConfirmDialog([
{
title: 'Confirm User Deletion',
message: `You are about to permanently delete ${selectedUsers.length} users.`
},
{
title: 'Final Confirmation',
message: 'This action cannot be undone. Type "DELETE" to confirm.',
requireInput: true,
expectedInput: 'DELETE'
}
]);
if (confirmed) {
await deleteUsersNetwork({
orgID: selectedOrg,
emailids: selectedAuditorList?.map((auditor: any) => auditor?.userID) || [],
});
// Log the deletion
await logDeletion({
deletedBy: currentUser.id,
orgID: selectedOrg,
deletedUsers: selectedAuditorList,
timestamp: new Date().toISOString()
});
handleClear();
setDeleteUserLoading(false);
fetchUserDetails();
toast.success('Users deleted successfully');
}
};
};Hard Delete Process:
- Organization Selection: CS team selects target organization
- User Selection: Multiple users can be selected for deletion
- Multi-Level Confirmation: Requires explicit confirmation
- Data Cleanup: Removes all user-related data
- Audit Logging: Comprehensive logging of deletion
3. Bulk Operations
The system supports bulk status changes through the bulk modal:
const handleBulkBlock = async (userIds: string[]) => {
// Filter out non-blockable users
const blockableUsers = userIds.filter(userId => {
const user = users[userId];
return !isNotBlockable(user) && userId !== currentUserId;
});
if (blockableUsers.length === 0) {
toast.error('No users can be blocked from the selection');
return;
}
if (blockableUsers.length < userIds.length) {
toast.warning(`${userIds.length - blockableUsers.length} users cannot be blocked`);
}
// Batch API calls
const results = await Promise.all(
blockableUsers.map(userId => updateUserStatus(userId, 'disabled'))
);
const successCount = results.filter(r => r.success).length;
toast.success(`Successfully blocked ${successCount} users`);
};4. Data Cleanup and Dependencies
When a user is deleted (hard delete), the system must handle:
- Site Assignments: Remove user from all site assignments
- Department Associations: Clear department memberships
- Reports: Archive or reassign user’s reports
- Authentication: Revoke all active sessions
- Notifications: Cancel pending notifications
const cleanupUserData = async (userId: string) => {
// Remove from sites
await removeSiteAssignments(userId);
// Clear department associations
await clearDepartmentMemberships(userId);
// Handle reports
const reports = await getUserReports(userId);
if (reports.length > 0) {
await archiveReports(reports);
}
// Revoke authentication
await revokeUserSessions(userId);
// Cancel notifications
await cancelPendingNotifications(userId);
// Remove from cache
await clearUserCache(userId);
};5. API Integration
Soft Delete (Block User):
PUT /api/v1/users/:userId/status
{
"status": "disabled",
"reason": "User blocked by admin",
"blockedBy": "adminUserId",
"blockedAt": "2024-01-20T10:00:00Z"
}Hard Delete (CS Utility):
DELETE /api/v1/organizations/:orgId/users
{
"userIds": ["user1", "user2", "user3"],
"confirmation": "DELETE",
"deletedBy": "csUserId",
"reason": "Customer request"
}Reactivate User:
PUT /api/v1/users/:userId/status
{
"status": "active",
"reactivatedBy": "adminUserId",
"reactivatedAt": "2024-01-20T11:00:00Z"
}6. Safety Measures
The module implements multiple safety measures:
1. Confirmation Dialogs:
const confirmDialog = async (options: ConfirmOptions) => {
return new Promise((resolve) => {
const modal = createConfirmModal({
...options,
onConfirm: () => {
resolve(true);
modal.close();
},
onCancel: () => {
resolve(false);
modal.close();
}
});
modal.open();
});
};2. Audit Logging:
const logUserDeletion = async (deletion: DeletionLog) => {
await auditLog.create({
action: 'USER_DELETED',
actor: deletion.deletedBy,
target: deletion.userId,
timestamp: deletion.timestamp,
details: {
userEmail: deletion.userEmail,
userName: deletion.userName,
reason: deletion.reason,
method: deletion.method // 'soft' or 'hard'
}
});
};3. Rollback Capability:
const createDeletionBackup = async (userId: string) => {
const userData = await getUserFullData(userId);
const backup = {
userId,
data: userData,
timestamp: new Date().toISOString(),
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) // 30 days
};
await backupStorage.save(backup);
return backup.id;
};7. Permission Matrix
| Role | Can Block Users | Can Unblock Users | Can Hard Delete |
|---|---|---|---|
| Superadmin | Yes (all) | Yes (all) | Yes |
| Account Holder | Yes (below level) | Yes (below level) | No |
| Admin | Yes (below level) | Yes (below level) | No |
| Supervisor | No | No | No |
| Auditor | No | No | No |
| CS Team | No | No | Yes (with approval) |
Error Handling
The module provides comprehensive error handling:
const handleDeleteError = (error: DeleteError) => {
switch (error.code) {
case 'PERMISSION_DENIED':
toast.error('You do not have permission to delete this user');
break;
case 'SELF_DELETE':
toast.error('You cannot delete your own account');
break;
case 'HIERARCHY_VIOLATION':
toast.error('Cannot delete users at or above your level');
break;
case 'ACTIVE_SESSIONS':
toast.error('User has active sessions. Please try again later');
break;
case 'DEPENDENCY_ERROR':
toast.error('User has dependencies that must be resolved first');
break;
default:
toast.error('Failed to delete user. Please try again');
}
};Best Practices
-
Always Prefer Soft Delete:
- Use status change to ‘disabled’ for regular operations
- Reserve hard delete for data compliance requirements
- Maintain audit trails for all deletions
-
Permission Validation:
- Check permissions both client and server side
- Enforce hierarchical access control
- Prevent self-deletion
-
User Communication:
- Clear confirmation dialogs
- Explain consequences of deletion
- Provide undo options where possible
-
Data Integrity:
- Handle all dependencies before deletion
- Create backups for hard deletes
- Maintain referential integrity
-
Audit and Compliance:
- Log all deletion operations
- Track who deleted whom and when
- Comply with data retention policies
Recovery Procedures
For accidental deletions:
-
Soft Delete Recovery:
// Simply reactivate the user await updateUserStatus(userId, 'active'); -
Hard Delete Recovery (within retention period):
const backup = await backupStorage.get(backupId); if (backup && !backup.expired) { await restoreUser(backup.data); await backupStorage.markAsRestored(backupId); }
Routes and Navigation
User Module Routes
The Users module exposes the following routes for navigation:
| Route | Component | Purpose | Access Control |
|---|---|---|---|
/admin/auditors | AuditorsPage | Main user list page | RoleResources.ADMIN_USER_ALL |
/admin/auditors/new | AuditorEditPage | Create new user | RoleResources.ADMIN_USER_ALL |
/admin/auditors/:userId | AuditorEditPage | Edit existing user | RoleResources.ADMIN_USER_ALL |
/admin/auditors/offdays | AuditorOffDaysPage | Manage auditor off days | RoleResources.ADMIN_NONOPERATIONALDAYS_ALL |
/admin/auditors/useroffdays | AuditorOffDaysPage | User-specific off days | RoleResources.ADMIN_NONOPERATIONALDAYS_ALL + Features.AUDITOR_NON_OPERATIONAL_DAYS |
/admin/auditors/useroffdays-dashboard | AuditorOffDaysDashboardPage | Off days dashboard | RoleResources.ADMIN_NONOPERATIONALDAYS_ALL + Features.AUDITOR_NON_OPERATIONAL_DAYS |
/analytics/auditors | AnalyticsAuditorsPage | User analytics | RoleResources.DASHBOARD_USERS_ALL |
/analytics/auditorDetails | AnalyticsAuditorDetailsPage | Detailed user analytics | Open access |
/analytics/auditorDetailsReport/:journeyCode | AnalyticsAuditorDetailsReportPage | User journey reports | Open access |
/customerSuccess/delete-users | DeleteUsers | CS utility for user deletion | Special CS permissions |
/customerSuccess/users-role-change | UsersRoleChange | CS utility for bulk role changes | Special CS permissions |
/customerSuccess/changeUserEmail | ChangeUserEmailPage | CS utility for email changes | Special CS permissions |
Navigation Flow
graph LR A[Dashboard] --> B[User Management /admin/auditors] B --> C[User List] C --> D[Create User /admin/auditors/new] C --> E[Edit User /admin/auditors/:id] C --> F[User Analytics] B --> G[Off Days Management] G --> H[General Off Days] G --> I[User Off Days] G --> J[Off Days Dashboard] K[CS Utilities] --> L[Delete Users] K --> M[Change Roles] K --> N[Change Email]
API Endpoints
Core User APIs
| Endpoint | Method | Purpose | Request Body | Response |
|---|---|---|---|---|
/api/v1/users | GET | List all users | Query params: page, limit, sortBy, sortDirection, filter, search | { users: User[], total: number, page: number } |
/api/v1/users/:userId | GET | Get single user | - | { user: User } |
/api/v1/users | POST | Create new user | { email, displayName, phoneNumber, role, departmentIDs, whatsappNumber, whatsappAdvanceNotification, siteSupervisors, siteAuditors, password } | { user: User, message: string } |
/api/v1/users/:userId | PUT | Update user | { email, displayName, phoneNumber, role, departmentIDs, whatsappNumber, whatsappAdvanceNotification, siteSupervisors, siteAuditors, password } | { user: User, message: string } |
/api/v1/users/:userId/status | PUT | Update user status | { status: 'active' | 'disabled', reason?: string } | { success: boolean, message: string } |
/api/v1/users/bulk-update | POST | Bulk update users | { userIds: string[], updates: Partial<User> } | { results: UpdateResult[] } |
/api/v1/users/check-availability | POST | Check username/email availability | { email: string } | { available: boolean } |
Department and Site Assignment APIs
| Endpoint | Method | Purpose | Request Body | Response |
|---|---|---|---|---|
/api/v1/users/:userId/departments | GET | Get user departments | - | { departments: Department[] } |
/api/v1/users/:userId/departments | PUT | Update user departments | { departmentIDs: string[] } | { success: boolean } |
/api/v1/users/:userId/site-assignments | GET | Get user site assignments | - | { supervisorSites: Site[], auditorSites: Site[] } |
/api/v1/users/:userId/site-assignments | PUT | Update site assignments | { siteSupervisors: SiteAssignment[], siteAuditors: SiteAssignment[] } | { success: boolean } |
User Role APIs
| Endpoint | Method | Purpose | Request Body | Response |
|---|---|---|---|---|
/user-roles/user-to-role | PUT | Assign role to user | { userID: string, role: string } | { success: boolean } |
/api/v1/roles | GET | Get all available roles | - | { roles: Role[] } |
/api/v1/roles/permissions | GET | Get role permissions | - | { permissions: RolePermission[] } |
Off Days Management APIs
| Endpoint | Method | Purpose | Request Body | Response |
|---|---|---|---|---|
/api/v1/auditor-off-days | GET | Get auditor off days | Query params: userId, startDate, endDate | { offDays: OffDay[] } |
/api/v1/auditor-off-days | POST | Create off day | { userId, date, reason, type } | { offDay: OffDay } |
/api/v1/auditor-off-days/:id | PUT | Update off day | { date, reason, type } | { offDay: OffDay } |
/api/v1/auditor-off-days/:id | DELETE | Delete off day | - | { success: boolean } |
Customer Success APIs
| Endpoint | Method | Purpose | Request Body | Response |
|---|---|---|---|---|
/api/v1/organizations/:orgId/users | DELETE | Hard delete users | { userIds: string[], confirmation: string, reason: string } | { deletedCount: number } |
/api/v1/users/change-email | POST | Change user email | { oldEmail: string, newEmail: string, userId: string } | { success: boolean } |
/api/v1/users/bulk-role-change | POST | Bulk role change | { userIds: string[], newRole: string } | { results: RoleChangeResult[] } |
Firebase Functions
| Function | Purpose | Parameters | Response |
|---|---|---|---|
createNewAuditor | Create user via Firebase | { email, displayName, phoneNumber } | { uid: string } |
updateExistingAuditor | Update Firebase user | { uid, email, displayName } | { success: boolean } |
deleteFirebaseUser | Delete Firebase user | { uid } | { success: boolean } |
Package Dependencies
Core Dependencies
| Package | Version | Purpose | Usage in Module |
|---|---|---|---|
react | ^17.0.2 | Core UI framework | All components |
react-redux | ^7.2.4 | State management | Redux integration |
redux | ^4.1.0 | State container | State management |
redux-saga | ^1.1.3 | Side effects | API calls, async operations |
react-router-dom | ^5.2.0 | Routing | Navigation between pages |
typescript | ^4.3.5 | Type safety | Type definitions |
UI and Styling
| Package | Version | Purpose | Usage in Module |
|---|---|---|---|
styled-components | ^5.3.0 | CSS-in-JS | Component styling |
react-toastify | ^7.0.4 | Toast notifications | User feedback |
@loadable/component | ^5.15.0 | Code splitting | Lazy loading components |
react-select | ^4.3.1 | Select components | Role, department selection |
Form and Validation
| Package | Version | Purpose | Usage in Module |
|---|---|---|---|
libphonenumber-js | ^1.9.23 | Phone validation | Phone number formatting |
papaparse | ^5.3.1 | CSV parsing | Bulk user import |
lodash | ^4.17.21 | Utilities | Debounce, cloneDeep |
Authentication and Security
| Package | Version | Purpose | Usage in Module |
|---|---|---|---|
react-redux-firebase | ^3.10.0 | Firebase integration | Authentication |
firebase | ^8.6.8 | Backend services | User management |
Nimbly-Specific Packages
| Package | Version | Purpose | Usage in Module |
|---|---|---|---|
@nimbly-technologies/nimbly-common | Latest | Common utilities | Enums, types, utilities |
@nimbly-technologies/audit-component | Latest | Audit components | UI components |
Development Dependencies
| Package | Version | Purpose | Usage in Module |
|---|---|---|---|
@types/react | ^17.0.11 | TypeScript types | Type definitions |
@types/react-redux | ^7.1.16 | Redux types | Type safety |
@types/styled-components | ^5.1.10 | Styled components types | Type safety |
@types/lodash | ^4.14.170 | Lodash types | Type definitions |
Analytics and Monitoring
| Package | Version | Purpose | Usage in Module |
|---|---|---|---|
react-ga | ^3.3.0 | Google Analytics | User action tracking |
Custom Monitoring | Internal | Error tracking | Error logging |
Utility Libraries
| Package | Version | Purpose | Usage in Module |
|---|---|---|---|
react-i18next | ^11.11.1 | Internationalization | Multi-language support |
axios | ^0.21.1 | HTTP client | API requests |
date-fns | ^2.22.1 | Date utilities | Date formatting |
Architecture Diagrams
Overall System Architecture
graph TB subgraph "Frontend Layer" A[React Components] B[Redux Store] C[Redux Sagas] end subgraph "Service Layer" D[User Service] E[Department Service] F[Site Service] G[Role Service] end subgraph "API Layer" H[REST APIs] I[Firebase Functions] end subgraph "Data Layer" J[Firebase Auth] K[Firestore DB] L[Cloud Storage] end A --> B B --> C C --> D C --> E C --> F C --> G D --> H E --> H F --> H G --> H H --> J H --> K I --> J I --> K A --> L
Component Hierarchy
graph TD A[App Root] --> B[Admin Routes] B --> C[AuditorsPage] C --> D[Layout] D --> E[AuditorManager] E --> F[AuditorListHeader] E --> G[AuditorList] E --> H[AuditorEditor Modal] E --> I[AuditorBulkModal] F --> J[Search Component] F --> K[Filter Components] F --> L[Action Buttons] G --> M[AuditorListContainer] M --> N[AuditorListTable] M --> O[AuditorListCard] M --> P[Pagination] H --> Q[AuditorEditorContainer] Q --> R[AuditorEditorForm] I --> S[BulkOperationForm] style A fill:#f9f,stroke:#333,stroke-width:4px style C fill:#bbf,stroke:#333,stroke-width:2px style E fill:#bfb,stroke:#333,stroke-width:2px
State Management Flow
stateDiagram-v2 [*] --> Idle Idle --> Loading: FETCH_USERS_REQUEST Loading --> Success: FETCH_USERS_SUCCESS Loading --> Error: FETCH_USERS_FAILURE Success --> Editing: EDIT_USER Success --> Creating: CREATE_USER Success --> Deleting: DELETE_USER Editing --> Updating: SUBMIT_FORM Creating --> Saving: SUBMIT_FORM Updating --> Success: UPDATE_SUCCESS Updating --> Error: UPDATE_FAILURE Saving --> Success: CREATE_SUCCESS Saving --> Error: CREATE_FAILURE Deleting --> Success: DELETE_SUCCESS Deleting --> Error: DELETE_FAILURE Error --> Idle: CLEAR_ERROR Success --> Idle: CLEAR_STATE
User Lifecycle
graph LR A[User Creation] --> B{Validation} B -->|Valid| C[Active User] B -->|Invalid| D[Creation Failed] C --> E[User Updates] C --> F[Status Changes] C --> G[Role Changes] E --> C F --> H[Disabled User] G --> C H --> I[Reactivation] I --> C H --> J[Hard Delete] J --> K[Deleted] C --> L[Soft Delete] L --> H
Permission Hierarchy
graph TD A[Superadmin Level 99] --> B[Account Holder Level 98] B --> C[Admin Level 3] C --> D[Supervisor Level 2] D --> E[Auditor Level 1] A -.->|Can manage| B A -.->|Can manage| C A -.->|Can manage| D A -.->|Can manage| E B -.->|Can manage| C B -.->|Can manage| D B -.->|Can manage| E C -.->|Can manage| D C -.->|Can manage| E D -.->|Can manage| E style A fill:#f96,stroke:#333,stroke-width:4px style B fill:#f93,stroke:#333,stroke-width:3px style C fill:#f90,stroke:#333,stroke-width:2px
Advanced Features
Off-Days Management System
The Users module includes a comprehensive off-days management system for tracking auditor availability:
Architecture
graph TD A[Off Days Management] --> B[General Off Days] A --> C[User-Specific Off Days] A --> D[Off Days Dashboard] B --> E[Public Holidays] B --> F[Company Holidays] C --> G[Personal Leave] C --> H[Sick Leave] C --> I[Other Leave] D --> J[Calendar View] D --> K[List View] D --> L[Analytics]
Implementation
Off Days Manager Component (src/components/auditors/offDaysManager/OffDaysManager.tsx):
const OffDaysManager = () => {
const [offDays, setOffDays] = useState<OffDay[]>([]);
const [selectedDates, setSelectedDates] = useState<Date[]>([]);
const handleAddOffDay = async (dates: Date[], reason: string, type: OffDayType) => {
const newOffDays = dates.map(date => ({
date: format(date, 'yyyy-MM-dd'),
reason,
type,
userId: selectedUserId
}));
const results = await Promise.all(
newOffDays.map(offDay => createOffDay(offDay))
);
toast.success(`Added ${results.length} off days`);
refreshOffDays();
};
const handleRemoveOffDay = async (offDayId: string) => {
await deleteOffDay(offDayId);
toast.success('Off day removed');
refreshOffDays();
};
};Features
-
Date Picker Integration:
- Multi-date selection
- Date range selection
- Blocked date highlighting
-
Types of Off Days:
- Personal leave
- Sick leave
- Public holidays
- Training days
- Other (custom reason)
-
Validation:
- Prevent past date selection (configurable)
- Conflict detection
- Maximum days limit
-
Dashboard Analytics:
- Off days by user
- Off days by type
- Monthly/yearly trends
- Team availability overview
Bulk Operations System
The module supports various bulk operations for efficient user management:
Bulk User Import
CSV Template Structure:
Email Address,Full Name,Role,Phone Number (e.g. +628123456789),Department,Site Assignment
john.doe@example.com,John Doe,Auditor,+628123456789,Sales,Site A|Supervisor;Site B|Auditor
jane.smith@example.com,Jane Smith,Supervisor,+628123456790,Marketing,Site A|SupervisorImport Process:
const processBulkImport = async (csvData: any[]) => {
const validationResults = validateImportData(csvData);
if (validationResults.errors.length > 0) {
showValidationErrors(validationResults.errors);
return;
}
const chunks = chunkArray(validationResults.valid, 5);
let processed = 0;
for (const chunk of chunks) {
const results = await Promise.all(
chunk.map(userData => createUserWithAssignments(userData))
);
processed += results.filter(r => r.success).length;
updateProgress(processed / validationResults.valid.length);
}
showImportSummary({
total: csvData.length,
successful: processed,
failed: csvData.length - processed
});
};Bulk Status Updates
const bulkStatusUpdate = async (userIds: string[], newStatus: UserStatus) => {
// Group by current status for validation
const usersByStatus = groupBy(userIds, userId => users[userId].status);
// Validate transitions
const validTransitions = validateStatusTransitions(usersByStatus, newStatus);
if (validTransitions.invalid.length > 0) {
toast.warning(`${validTransitions.invalid.length} users cannot be updated`);
}
// Execute updates
const results = await batchUpdate(validTransitions.valid, {
status: newStatus,
updatedBy: currentUser.id,
updatedAt: new Date().toISOString()
});
return results;
};Advanced Search and Filtering
The Users module implements sophisticated search capabilities:
Search Implementation
const useUserSearch = () => {
const [searchTerm, setSearchTerm] = useState('');
const [filters, setFilters] = useState<UserFilters>({
status: 'all',
role: 'all',
department: 'all',
site: 'all'
});
const debouncedSearch = useMemo(
() => debounce((term: string) => {
dispatch(searchUsers(term));
}, 300),
[]
);
const filteredUsers = useMemo(() => {
let result = Object.values(users);
// Text search
if (searchTerm) {
result = result.filter(user =>
user.email.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.displayName.toLowerCase().includes(searchTerm.toLowerCase()) ||
user.phoneNumber?.includes(searchTerm)
);
}
// Status filter
if (filters.status !== 'all') {
result = result.filter(user => user.status === filters.status);
}
// Role filter
if (filters.role !== 'all') {
result = result.filter(user => user.role === filters.role);
}
// Department filter
if (filters.department !== 'all') {
result = result.filter(user =>
user.departments?.includes(filters.department)
);
}
// Site filter
if (filters.site !== 'all') {
result = result.filter(user => {
const siteIds = [
...user.siteSupervisors?.map(s => s.siteID) || [],
...user.siteAuditors?.map(s => s.siteID) || []
];
return siteIds.includes(filters.site);
});
}
return result;
}, [users, searchTerm, filters]);
return {
searchTerm,
setSearchTerm,
filters,
setFilters,
filteredUsers,
debouncedSearch
};
};Advanced Filters
-
Multi-Select Filters:
const multiSelectFilter = (users: User[], filterKey: string, values: string[]) => { if (values.length === 0) return users; return users.filter(user => { const userValues = user[filterKey] || []; return values.some(value => userValues.includes(value)); }); }; -
Date Range Filters:
const dateRangeFilter = (users: User[], startDate: Date, endDate: Date) => { return users.filter(user => { const createdDate = new Date(user.createdAt); return createdDate >= startDate && createdDate <= endDate; }); }; -
Custom Filters:
const customFilters = { hasWhatsApp: (user: User) => !!user.whatsappNumber, isActive: (user: User) => user.status === 'active', hasMultipleSites: (user: User) => { const totalSites = (user.siteSupervisors?.length || 0) + (user.siteAuditors?.length || 0); return totalSites > 1; }, noRecentActivity: (user: User) => { const lastActive = new Date(user.lastActiveAt); const thirtyDaysAgo = subDays(new Date(), 30); return lastActive < thirtyDaysAgo; } };
Analytics and Reporting
The Users module provides comprehensive analytics:
User Analytics Dashboard
const UserAnalytics = () => {
const metrics = useUserMetrics();
return (
<Dashboard>
<MetricCard
title="Total Users"
value={metrics.totalUsers}
change={metrics.userGrowth}
icon={<UsersIcon />}
/>
<MetricCard
title="Active Users"
value={metrics.activeUsers}
percentage={metrics.activePercentage}
icon={<ActivityIcon />}
/>
<Chart
type="pie"
data={metrics.usersByRole}
title="Users by Role"
/>
<Chart
type="bar"
data={metrics.usersByDepartment}
title="Users by Department"
/>
<ActivityHeatmap
data={metrics.loginActivity}
title="Login Activity"
/>
</Dashboard>
);
};Report Generation
const generateUserReport = async (reportType: ReportType, filters: ReportFilters) => {
const data = await fetchReportData(reportType, filters);
switch (reportType) {
case 'user-list':
return generateUserListReport(data);
case 'activity-summary':
return generateActivityReport(data);
case 'role-distribution':
return generateRoleReport(data);
case 'department-allocation':
return generateDepartmentReport(data);
default:
throw new Error('Unknown report type');
}
};Integration Points
LMS Integration
The Users module integrates with the Learning Management System:
const syncUserWithLMS = async (userId: string) => {
const user = await getUser(userId);
const lmsUser = {
id: user.id,
email: user.email,
name: user.displayName,
role: mapRoleToLMS(user.role),
departments: user.departments,
active: user.status === 'active'
};
await lmsService.upsertUser(lmsUser);
// Assign default courses based on role
const defaultCourses = await getDefaultCoursesForRole(user.role);
await lmsService.assignCourses(user.id, defaultCourses);
};Site Scheduling Integration
Users are integrated with site scheduling:
const getUserAvailability = async (userId: string, startDate: Date, endDate: Date) => {
const [schedule, offDays] = await Promise.all([
getSiteSchedule(userId, startDate, endDate),
getOffDays(userId, startDate, endDate)
]);
const availability = eachDayOfInterval({ start: startDate, end: endDate })
.map(date => {
const dateStr = format(date, 'yyyy-MM-dd');
const isOffDay = offDays.some(od => od.date === dateStr);
const scheduledSites = schedule.filter(s => s.date === dateStr);
return {
date: dateStr,
available: !isOffDay,
scheduledSites,
capacity: isOffDay ? 0 : (MAX_SITES_PER_DAY - scheduledSites.length)
};
});
return availability;
};Performance Optimization
Virtual Scrolling
For large user lists, the module implements virtual scrolling:
const VirtualizedUserList = ({ users, rowHeight = 50 }) => {
const listRef = useRef<VariableSizeList>(null);
const [scrollOffset, setScrollOffset] = useState(0);
const getItemSize = (index: number) => {
// Variable heights for expanded rows
return expandedRows[index] ? rowHeight * 2 : rowHeight;
};
const Row = ({ index, style }) => {
const user = users[index];
return (
<div style={style}>
<UserRow user={user} expanded={expandedRows[index]} />
</div>
);
};
return (
<VariableSizeList
ref={listRef}
height={600}
itemCount={users.length}
itemSize={getItemSize}
onScroll={({ scrollOffset }) => setScrollOffset(scrollOffset)}
>
{Row}
</VariableSizeList>
);
};Memoization Strategies
// Memoized selectors
const getUsersByRole = createSelector(
[getUsers, getSelectedRole],
(users, role) => {
if (role === 'all') return users;
return Object.values(users).filter(user => user.role === role);
}
);
// Memoized components
const UserRow = memo(({ user, onEdit, onBlock }) => {
return (
<tr>
<td>{user.email}</td>
<td>{user.displayName}</td>
<td>{user.role}</td>
<td>
<Button onClick={() => onEdit(user.id)}>Edit</Button>
<Button onClick={() => onBlock(user.id)}>Block</Button>
</td>
</tr>
);
}, (prevProps, nextProps) => {
return prevProps.user.id === nextProps.user.id &&
prevProps.user.status === nextProps.user.status;
});Testing Strategy
Unit Tests
describe('UserService', () => {
describe('createUser', () => {
it('should create user with valid data', async () => {
const userData = {
email: 'test@example.com',
displayName: 'Test User',
role: 'auditor'
};
const result = await userService.createUser(userData);
expect(result).toHaveProperty('id');
expect(result.email).toBe(userData.email);
expect(result.status).toBe('active');
});
it('should reject duplicate email', async () => {
const userData = {
email: 'existing@example.com',
displayName: 'Test User',
role: 'auditor'
};
await expect(userService.createUser(userData))
.rejects.toThrow('Email already exists');
});
});
});Integration Tests
describe('User Management Flow', () => {
it('should complete full user lifecycle', async () => {
// Create user
const { getByText, getByLabelText } = render(<AuditorManager />);
fireEvent.click(getByText('Add User'));
fireEvent.change(getByLabelText('Email'), {
target: { value: 'newuser@example.com' }
});
fireEvent.change(getByLabelText('Name'), {
target: { value: 'New User' }
});
fireEvent.click(getByText('Save'));
await waitFor(() => {
expect(getByText('User created successfully')).toBeInTheDocument();
});
// Update user
const userRow = getByText('newuser@example.com').closest('tr');
fireEvent.click(within(userRow).getByText('Edit'));
fireEvent.change(getByLabelText('Phone'), {
target: { value: '+628123456789' }
});
fireEvent.click(getByText('Update'));
await waitFor(() => {
expect(getByText('User updated successfully')).toBeInTheDocument();
});
// Block user
fireEvent.click(within(userRow).getByText('Block'));
fireEvent.click(getByText('Confirm'));
await waitFor(() => {
expect(getByText('User blocked successfully')).toBeInTheDocument();
});
});
});