Overview
The Department Module is a comprehensive administrative system within the Nimbly audit-admin platform that provides functionality for managing organizational departments and department groups. This module enables organizations to create, manage, and organize departments with hierarchical user assignments, site associations, questionnaire mappings, and sophisticated escalation workflows.
Core Functionality
The department module provides the following key capabilities:
- Department Management: Create, update, delete, and manage departments within an organization
- Department Groups: Organize multiple departments into logical groups for better management
- User Assignment: Assign users to departments with different hierarchy levels
- Site Association: Link sites/locations to specific departments
- Questionnaire Mapping: Associate questionnaires with departments for targeted audits
- Issue Escalation: Configure multi-level escalation workflows with time-based triggers
- Bulk Operations: Support for bulk department creation via Excel upload
- Status Management: Manage active/inactive states for departments and groups
Module Architecture Overview
graph TB subgraph "Frontend Layer" Pages[Pages Layer] Components[Component Layer] Redux[Redux State Management] Sagas[Redux Sagas] end subgraph "API Layer" REST[REST APIs] CloudV2[Cloud V2 APIs] end subgraph "Backend Services" DeptService[Department Service] IndexService[Department Index Service] GroupService[Department Group Service] end Pages --> Components Components --> Redux Redux --> Sagas Sagas --> REST Sagas --> CloudV2 REST --> DeptService REST --> IndexService CloudV2 --> DeptService REST --> GroupService
Architecture
Component Structure
The department module follows a modular component architecture with clear separation of concerns:
src/
├── pages/
│ ├── departments.js # Department listing page
│ ├── departments-edit.tsx # Department editor page
│ └── admin/departmentsgroup/ # Department groups pages
├── components/departments/
│ ├── DepartmentManager.js # Main department manager
│ ├── DepartmentList.js # Department list display
│ ├── DepartmentListHeader/ # List header component
│ ├── DepartmentEditor/ # Department CRUD editor
│ ├── DepartmentBulkModal.tsx # Bulk upload modal
│ ├── DepartmentDeleteModal/ # Delete confirmation modal
│ ├── DepartmentNavigation.tsx # Navigation tabs
│ ├── DepartmentGroupManager/ # Group management
│ ├── DepartmentGroupList/ # Group listing
│ ├── DepartmentGroupEditor/ # Group CRUD editor
│ └── utils/ # Utility functions
├── reducers/
│ ├── departments/ # Department state management
│ ├── departmentEditor/ # Editor state management
│ └── departmentGroup/ # Group state management
├── sagas/
│ ├── departments/ # Department side effects
│ ├── departmentEditor/ # Editor side effects
│ └── DepartmentGroup/ # Group side effects
└── models/
└── departmentIndex.ts # Department index model
State Management Architecture
The module uses Redux for state management with the following structure:
graph LR subgraph "Redux Store" DeptState[departments] EditorState[departmentEditor] GroupState[departmentGroup] end subgraph "Actions" DeptActions[Department Actions] EditorActions[Editor Actions] GroupActions[Group Actions] end subgraph "Sagas" DeptSagas[Department Sagas] EditorSagas[Editor Sagas] GroupSagas[Group Sagas] end DeptActions --> DeptState EditorActions --> EditorState GroupActions --> GroupState DeptSagas --> DeptActions EditorSagas --> EditorActions GroupSagas --> GroupActions
Data Models and Types
Department Model
interface Department {
departmentID: string; // Unique department identifier
name: string; // Department name
description: string; // Department description
email: string; // Department contact email
status: 'active' | 'disabled'; // Department status
organizationID: string; // Parent organization ID
createdAt?: Date;
updatedAt?: Date;
}Department Index Model
interface DepartmentIndex {
ID: string; // Document ID
departmentID: string; // Department reference
organizationID: string; // Organization reference
name: string;
description: string;
email: string;
disabled: boolean;
defaultIssueOwner?: string; // Default issue assignee
users: Array<{ // User assignments
uid: string;
level: number; // Hierarchy level (1, 2, 3...)
}>;
sites: Array<{ // Associated sites
id: string;
}>;
questionnaires: Array<{ // Associated questionnaires
id: string;
}>;
approvers: Array<{ // Issue approvers
id: string;
}>;
escalateTime: { // Escalation configuration
[level: number]: number | null; // Hours to escalate
};
}Department Group Model
interface DepartmentGroup {
ID: string; // Group identifier
name: string; // Group name
description: string; // Group description
status: 'active' | 'disabled'; // Group status
departments: Array<{ // Associated departments
id: string;
}>;
organizationID: string;
createdAt?: Date;
updatedAt?: Date;
}State Models
interface DepartmentsState {
// Filter and Sort
filterQuery: string;
sortBy: string;
key: 'asc' | 'desc';
name: 'asc' | 'desc';
description: 'asc' | 'desc';
email: 'asc' | 'desc';
// Selected Items
selectedDepartmentKey: string | null;
selectedDepartment: Department | null;
selectedDepartmentDeleteKey: string | null;
selectedTab: string;
// Data
departments: { [key: string]: Department } | null;
departmentsPaginate: { [key: string]: Department } | null;
departmentIndex: DepartmentIndex | null;
departmentsIndex: { [key: string]: DepartmentIndex } | null;
departmentIndexWithRoles: DepartmentIndexUserInSite | null;
// Loading States
isLoaded: boolean;
isLoadedDepartments: boolean;
isLoadingDepartments: boolean;
isLoadingDepartmentIndex: boolean;
isLoadingDepartmentIndexWithRoles: boolean;
// Pagination
pageIndex: number;
totalDepartments: number | null;
// UI States
showDepartmentBulkModal: boolean;
showDepartmentDeleteModal: boolean;
// Schedule Data
isLoadingSchedule: boolean;
departmentSchedule: any;
// Error Handling
error: string | null;
}Department Management
Department Creation Flow
The department creation process involves multiple steps and validations:
sequenceDiagram participant User participant UI participant Redux participant Saga participant API User->>UI: Click "Add Department" UI->>UI: Navigate to /admin/departments/new UI->>User: Display Department Editor Form User->>UI: Fill Department Details Note over UI: Name, Description, Email, Key UI->>API: Check Department Key Availability API-->>UI: Key Available/Unavailable User->>UI: Assign Users, Sites, Questionnaires User->>UI: Configure Escalation Levels User->>UI: Click Save UI->>Redux: Dispatch createDepartment.request Redux->>Saga: Handle Creation Saga->>API: POST /departments/ API-->>Saga: Success/Error Response Saga->>Redux: Update State Redux->>UI: Show Success/Error UI->>User: Navigate to Department List
Department Update Flow
sequenceDiagram participant User participant UI participant Redux participant Saga participant API User->>UI: Click Edit Department UI->>Redux: Fetch Department Index Redux->>Saga: fetchDepartmentIndexById Saga->>API: GET /departments/index/{deptID} API-->>Saga: Department Data Saga->>Redux: Update State Redux->>UI: Populate Form User->>UI: Modify Department Details User->>UI: Click Save UI->>Redux: Dispatch updateDepartment.request Redux->>Saga: Handle Update Saga->>API: PUT /departments/index/{deptID} API-->>Saga: Success/Error Response Saga->>Redux: Update State Redux->>UI: Show Success/Error
Department Deletion/Deactivation
Departments are not physically deleted but rather deactivated:
// Department status toggle flow
const toggleDepartmentStatus = async (departmentKey: string) => {
// 1. Check for active schedules
const schedules = await fetchDepartmentSchedule(departmentKey);
// 2. If schedules exist, show warning
if (schedules.length > 0) {
showWarningModal("Department has active schedules");
return;
}
// 3. Proceed with status toggle
await updateDepartmentStatus(departmentKey, 'disabled');
};Bulk Department Upload
The system supports bulk department creation via Excel upload:
// Bulk upload process
const bulkUploadDepartments = async (file: File) => {
// 1. Parse Excel file
const departments = await parseExcelFile(file);
// 2. Validate each department
const validationResults = validateDepartments(departments);
// 3. If errors, display them
if (validationResults.errors.length > 0) {
displayErrors(validationResults.errors);
return;
}
// 4. Upload valid departments
await uploadBulkDepartments(validationResults.valid);
};Department Validation Rules
-
Department Key Validation:
- Must be unique within organization
- Alphanumeric characters and hyphens only
- No special characters or spaces
- Case-sensitive
-
Required Fields:
- Department Name
- Department Description
- Department Email (valid email format)
- Department Key
-
Assignment Validations:
- At least one user must be assigned
- Users can only be in one level per department
- Sites can be assigned to multiple departments
- Questionnaires can be shared across departments
Department Group Management
Department Group Creation
flowchart TD A[Start] --> B[Navigate to Department Groups] B --> C[Click Create New Group] C --> D[Enter Group Details] D --> E{Validate Input} E -->|Invalid| F[Show Validation Errors] F --> D E -->|Valid| G[Select Departments] G --> H{At Least One Dept?} H -->|No| I[Show Error] I --> G H -->|Yes| J[Save Group] J --> K[API Call] K --> L[Update Redux State] L --> M[Navigate to Group List] M --> N[End]
Group Management Features
-
Group Operations:
- Create new department groups
- Edit existing groups (name, description, departments)
- Activate/Deactivate groups
- Delete groups (soft delete)
-
Department Assignment:
- Add/remove departments from groups
- Bulk select all departments
- Visual department count display
-
Group Filtering:
- Search by group name
- Filter by status (active/disabled)
- Sort alphabetically
API Endpoints
Department APIs
| Method | Endpoint | Description | File Reference |
|---|---|---|---|
| GET | /departments | Fetch all departments | departments.actionSaga.ts:178 |
| GET | /departments/paginate | Fetch paginated departments | departments.actionSaga.ts:307 |
| GET | /departments/index/ | Fetch all department indices | departments.actionSaga.ts:28 |
| GET | /departments/index/{deptID} | Fetch specific department index | departments.actionSaga.ts:85 |
| POST | /departments/ | Create new department | departmentEditor.actionSaga.ts:32 |
| PUT | /departments/index/{deptID} | Update department | departmentEditor.actionSaga.ts:67 |
| GET | /departments/open/department/{org}/{key} | Check department key availability | DepartmentEditor.tsx:488 |
| GET | /schedules/departments/{id} | Fetch department schedules | departments.actionSaga.ts:350 |
| GET | /departments/user-filter-options | Fetch departments for user filtering | departments.actionSaga.ts:233 |
Department Group APIs
| Method | Endpoint | Description | File Reference |
|---|---|---|---|
| GET | /departments/group | Fetch all department groups | DepartmentGroup.Saga.ts:26 |
| GET | /departments/group/{groupDeptID} | Fetch specific group | DepartmentGroup.Saga.ts:44 |
| POST | /departments/group | Create department group | DepartmentGroupEditor.Saga.ts |
| PUT | /departments/group/{groupDeptID} | Update department group | DepartmentGroupEditor.Saga.ts |
| PUT | /departments/group/toggle-status/{groupDeptID} | Toggle group status | DepartmentGroup.Saga.ts:80 |
Cloud V2 APIs
| Method | Endpoint | Description | File Reference |
|---|---|---|---|
| GET | /admin/department-indexes/{deptID}?qType=user-role-in-site | Fetch department with user roles | departments.actionSaga.ts:115 |
| PUT | /admin/sites/replace-supervisors | Replace department supervisors | departments.actionSaga.ts:145 |
State Management
Redux Actions
Department Actions
// Fetch Actions
fetchDepartments.request()
fetchDepartments.success(data)
fetchDepartments.failure(error)
fetchDepartmentIndexById.request({ deptID })
fetchDepartmentIndexById.success({ data })
fetchDepartmentIndexById.failure({ error })
fetchPaginateDepartments.request({ limit })
fetchPaginateDepartments.success({ data, total })
fetchPaginateDepartments.failure({ error })
// UI Actions
setDepartmentsFilterQuery(text)
clearDepartmentsFilterQuery()
setSortDepartment(type, order)
setPageIndex(value)
setSelectedTab(value)
setShowDepartmentBulkModal(value)
setShowDepartmentDeleteModal(value)
setDimissDepartmentDeleteModal()Department Editor Actions
// CRUD Actions
createDepartment.request({ data })
createDepartment.success()
createDepartment.failure({ error })
updateDepartment.request({ deptID, data, assignSupervisor })
updateDepartment.success()
updateDepartment.failure({ error, errorData })
// UI Actions
setShowModal(value)
setIsSuccess(value)Department Group Actions
// Fetch Actions
getDepartmentGroup.request()
getDepartmentGroup.success({ data })
getDepartmentGroup.failure({ error })
getDepartmentGroupById.request({ groupDeptID })
getDepartmentGroupById.success({ data })
getDepartmentGroupById.failure({ error })
// CRUD Actions
createDepartmentGroup.request({ data })
createDepartmentGroup.success()
createDepartmentGroup.failure({ error })
updateDepartmentGroup.request({ groupDeptID, data })
updateDepartmentGroup.success()
updateDepartmentGroup.failure({ error })
deleteDepartmentGroup.request({ groupDeptID })
deleteDepartmentGroup.success({ newDeptGroup, selectedDepartment })
deleteDepartmentGroup.failure({ error })
// UI Actions
setDeptGroupActiveTab(tab)
setEditingDetail(value)Redux Sagas
The module uses Redux-Saga for handling side effects:
graph TD subgraph "Department Sagas" A[fetchDepartments] --> B[API Call] B --> C{Success?} C -->|Yes| D[Update State] C -->|No| E[Handle Error] F[fetchPaginateDepartments] --> G[Build Query] G --> H[API Call with Pagination] H --> I[Update Paginated State] end subgraph "Editor Sagas" J[createDepartment] --> K[Validate Data] K --> L[API POST] L --> M[Show Toast] M --> N[Navigate Away] O[updateDepartment] --> P[Check Supervisors] P --> Q[API PUT] Q --> R[Clear Cache] R --> S[Update State] end
State Selectors
// Common selectors used in the module
const selectDepartments = (state: RootState) => state.departments.departments;
const selectDepartmentsPaginate = (state: RootState) => state.departments.departmentsPaginate;
const selectDepartmentIndex = (state: RootState) => state.departments.departmentIndex;
const selectIsLoading = (state: RootState) => state.departments.isLoadingDepartments;
const selectActiveTab = (state: RootState) => state.departments.selectedTab;
const selectFilterQuery = (state: RootState) => state.departments.filterQuery;
const selectPageIndex = (state: RootState) => state.departments.pageIndex;
const selectTotalDepartments = (state: RootState) => state.departments.totalDepartments;
// Department Group selectors
const selectDepartmentGroups = (state: RootState) => state.departmentGroup.departmentGroup;
const selectDepartmentGroupById = (state: RootState) => state.departmentGroup.departmentGroupById;
const selectGroupActiveTab = (state: RootState) => state.departmentGroup.listActiveTab;
const selectEditingDetail = (state: RootState) => state.departmentGroup.editingDetail;Component Architecture
Component Hierarchy
graph TD subgraph "Department Pages" DeptPage[departments.js] DeptEditPage[departments-edit.tsx] DeptGroupPage[departmentsgroup] end subgraph "Department Components" DeptManager[DepartmentManager] DeptList[DepartmentList] DeptHeader[DepartmentListHeader] DeptNav[DepartmentNavigation] DeptEditor[DepartmentEditor] DeptEditorContainer[DepartmentEditorContainer] DeptBulkModal[DepartmentBulkModal] DeptDeleteModal[DepartmentDeleteModal] end subgraph "Department Group Components" GroupManager[DepartmentGroupManager] GroupList[DepartmentGroupList] GroupEditor[DepartmentGroupEditor] GroupEditorContainer[DepartmentGroupEditorContainer] end DeptPage --> DeptManager DeptManager --> DeptHeader DeptManager --> DeptNav DeptManager --> DeptList DeptManager --> DeptBulkModal DeptManager --> DeptDeleteModal DeptEditPage --> DeptEditor DeptEditor --> DeptEditorContainer DeptGroupPage --> GroupManager GroupManager --> DeptNav GroupManager --> GroupList GroupManager --> GroupEditor GroupEditor --> GroupEditorContainer
Key Components
DepartmentManager Component
The main container component that orchestrates the department listing functionality:
// Key responsibilities:
// 1. Fetches and displays department list
// 2. Handles pagination and filtering
// 3. Manages active/inactive tabs
// 4. Coordinates delete operations
// 5. Handles bulk upload modal
const DepartmentManager = (props) => {
// Pagination calculation based on window height
const deptItemListLimit = React.useMemo(() => {
const deductor = [40, 30, 36, 20, 24, 34, 30, 20];
const deductorTotal = deductor.reduce((curr, acc) => curr + acc);
const itemHeight = 30;
const itemPadding = 3;
return Math.ceil((windowHeight - deductorTotal) / (itemHeight + itemPadding));
}, [windowHeight]);
// Fetch departments on mount and filter changes
useEffect(() => {
props.fetchPaginateDepartments({ limit: deptItemListLimit });
}, [activeTab, filterQuery, name, email, description, key, pageIndex, sortBy]);
};DepartmentEditor Component
Complex form component handling department creation and editing:
// Key features:
// 1. Dynamic user assignment with hierarchy levels
// 2. Site and questionnaire association
// 3. Escalation time configuration
// 4. Real-time validation
// 5. Supervisor replacement handling
const DepartmentEditor = (props) => {
// State management for form fields
const [departmentName, setDepartmentName] = useState('');
const [departmentCode, setDepartmentCode] = useState('');
const [assignSites, setAssignSites] = useState<AssignItem[]>([]);
const [assignQuestionnaires, setAssignQuestionnaires] = useState<AssignItem[]>([]);
const [userLevels, setUserLevels] = useState<{ [level: string]: { users: {} } }>({});
const [escalationTime, setEscalationTime] = useState<{ [level: number]: { label: string; value: number } | null }>({});
// Escalation level management
const handleAddLevel = () => {
const newLevel = Math.max(...departmentLevels) + 1;
setUserLevels({
...userLevels,
[newLevel]: { users: {} },
});
setDepartmentLevels([...departmentLevels, newLevel]);
};
};DepartmentGroupManager Component
Manages the department group listing and editing interface:
// Split-view interface for group management
// Left panel: Group list
// Right panel: Group editor
const DepartmentGroupManager = () => {
// Auto-navigation to first group
useEffect(() => {
if (!isDeptGroupListLoading && departmentGroup) {
const filteredDepartmentGroup = departmentGroup
.filter((d) => d.status === activeTab)
.sort((a, b) => (a?.name?.toLowerCase() > b?.name?.toLowerCase() ? 1 : -1));
if (filteredDepartmentGroup.length) {
const activeDepartments = filteredDepartmentGroup[0];
history.push('/admin/departmentsgroup/' + activeDepartments.ID);
}
}
}, [location, isDeptGroupListLoading, activeTab]);
};Component Communication
sequenceDiagram participant User participant DeptManager participant Redux participant DeptList participant DeptEditor User->>DeptManager: View Departments DeptManager->>Redux: Fetch Departments Redux-->>DeptManager: Department Data DeptManager->>DeptList: Pass Department Props DeptList->>User: Display Departments User->>DeptList: Click Edit DeptList->>DeptManager: Handle Edit Click DeptManager->>User: Navigate to Editor User->>DeptEditor: Modify Department DeptEditor->>Redux: Update Department Redux-->>DeptEditor: Success DeptEditor->>User: Navigate Back
Business Logic and Calculations
Escalation Time Logic
The escalation system allows issues to be automatically escalated to higher-level users after specified time periods:
// Escalation time configuration
const escalationTimeLogic = {
// Time options available for selection
timeOptions: [
{ value: -1, label: 'No Escalation' },
{ value: 1, label: '1 hour' },
{ value: 2, label: '2 hours' },
{ value: 24, label: '1 day' },
{ value: 48, label: '2 days' },
{ value: 72, label: '3 days' },
{ value: 168, label: '1 week' },
// ... more options
],
// Validation rules
rules: {
// 1. Previous level must have escalation time before next level
// 2. Cannot skip levels
// 3. Level 1 always starts escalation chain
// 4. "No Escalation" stops the chain
},
// Example configuration:
// Level 1: Escalate after 2 hours
// Level 2: Escalate after 1 day
// Level 3: No escalation (final level)
};User Level Management
flowchart TD A[User Assignment] --> B{Check Existing Assignment} B -->|Not Assigned| C[Add to Available Users] B -->|Already Assigned| D[Show in Assigned Users] E[Add User to Level] --> F[Remove from Available] F --> G[Add to Level Users] G --> H[Update Assigned List] I[Remove User from Level] --> J[Remove from Level] J --> K[Add to Available] K --> L[Update Lists] M[Delete Level] --> N{Users in Level?} N -->|Yes| O[Show Warning] N -->|No| P[Delete Level] P --> Q[Reindex Remaining Levels]
Department Key Validation
const validateDepartmentKey = async (key: string): Promise<ValidationResult> => {
// 1. Format validation
const format = /[ `!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/;
if (format.test(key)) {
return {
valid: false,
error: 'Department key cannot contain special characters'
};
}
// 2. Uniqueness check
const response = await checkKeyAvailability(organization, key);
if (response.data !== null) {
return {
valid: false,
error: 'Department key already exists'
};
}
return { valid: true };
};Pagination Calculation
Dynamic pagination based on viewport height:
const calculatePaginationLimit = (windowHeight: number): number => {
const fixedHeights = {
rootPadding: 40,
headerHeight: 30,
headerMargin: 36,
tabHeight: 20,
tabMargin: 24,
tableHeader: 34,
pagination: 30,
paginationMargin: 20
};
const totalFixed = Object.values(fixedHeights).reduce((sum, height) => sum + height, 0);
const itemHeight = 30;
const itemPadding = 3;
return Math.ceil((windowHeight - totalFixed) / (itemHeight + itemPadding));
};Supervisor Replacement Logic
When deleting a user who is a supervisor in sites:
const handleSupervisorReplacement = async (
departmentID: string,
previousSupervisor: string,
replacementSupervisor: string,
siteID: string
) => {
// 1. Check if user is supervisor in any sites
const supervisorSites = await checkUserSupervisorRole(previousSupervisor);
// 2. If supervisor, show replacement modal
if (supervisorSites.length > 0) {
const replacement = await showReplacementModal(supervisorSites);
// 3. Call replacement API
await replaceSupervisors({
departmentID,
previousSupervisor,
replacementSupervisor: replacement.uid,
siteID: replacement.siteID
});
}
// 4. Remove user from department
removeUserFromDepartment(departmentID, previousSupervisor);
};Routing and Navigation
Route Configuration
| Route Path | Component | Purpose | Access Control |
|---|---|---|---|
/admin/departments | DepartmentsPage | Department listing | ADMIN_DEPARTMENT_ALL |
/admin/departments/new | DepartmentEditorPage | Create new department | ADMIN_DEPARTMENT_ALL |
/admin/departments/:departmentKey | DepartmentEditorPage | Edit existing department | ADMIN_DEPARTMENT_ALL |
/admin/departmentsgroup | DepartmentsGroupContainer | Department groups listing | ADMIN_DEPARTMENT_ALL |
/admin/departmentsgroup/:deptId | DepartmentsGroupContainer | View/Edit department group | ADMIN_DEPARTMENT_ALL |
Navigation Flow
graph LR A[Department List] -->|Create| B[New Department] A -->|Edit| C[Edit Department] A -->|Groups Tab| D[Department Groups] B -->|Save| A B -->|Cancel| A C -->|Save| A C -->|Cancel| A D -->|Departments Tab| A D -->|Create Group| E[New Group Editor] D -->|Edit Group| F[Edit Group Editor] E -->|Save| D F -->|Save| D
Route Guards
// Route protection using RouteWithValidation component
<Route
exact
path="/admin/departments"
component={DepartmentsPage}
withValidation
access={RoleResources.ADMIN_DEPARTMENT_ALL}
/>
// Access control checks:
// 1. User authentication
// 2. Role-based permissions
// 3. Feature flags
// 4. Organization settingsUser Interface and Interactions
Department List Interface
┌─────────────────────────────────────────────────────────────┐
│ Department Manager │
├─────────────────────────────────────────────────────────────┤
│ [Search...] [🔍] [+ Add Department] │
├─────────────────────────────────────────────────────────────┤
│ Departments | Department Groups │
├─────────────────────────────────────────────────────────────┤
│ Active | Inactive │
├─────────────────────────────────────────────────────────────┤
│ Key ↓ | Name ↓ | Description | Email | Actions │
├─────────────────────────────────────────────────────────────┤
│ SALES | Sales | Sales team | s@co | [✏️] [🚫] │
│ MKTG | Market | Marketing | m@co | [✏️] [🚫] │
├─────────────────────────────────────────────────────────────┤
│ < 1 2 3 ... 10 > │
└─────────────────────────────────────────────────────────────┘
Department Editor Interface
┌─────────────────────────────────────────────────────────────┐
│ Sales Department [Save] │
├─────────────────────────────────────────────────────────────┤
│ Department Details Assignments │
│ ┌─────────────────────┐ ┌─────────────────────┐│
│ │Name: [Sales Dept ]│ │Issue Owner: ││
│ │Desc: [Sales team ]│ │[Select user... ▼] ││
│ │Email: [sales@co ]│ │ ││
│ │Key: [SALES] [Check]│ │Sites: ││
│ └─────────────────────┘ │[Select sites... ▼] ││
│ │┌─────────────────┐ ││
│ Escalation Configuration ││Site A [x] │ ││
│ ┌─────────────────────┐ ││Site B [x] │ ││
│ │Level-1 → □ Notify: │ │└─────────────────┘ ││
│ │ [Select users ▼] │ │ ││
│ │ User A [x] │ │Questionnaires: ││
│ │ ↓ After [2 hours▼]│ │[Select... ▼] ││
│ │Level-2 → □ Notify: │ │┌─────────────────┐ ││
│ │ [Select users ▼] │ ││Quest A [x] │ ││
│ │ User B [x] │ ││Quest B [x] │ ││
│ └─────────────────────┘ │└─────────────────┘ ││
│ [+ Add Level] └─────────────────────┘│
└─────────────────────────────────────────────────────────────┘
Interaction Patterns
-
Search and Filter:
- Real-time search with 1-second debounce
- Filters apply to key, name, description, email
- Maintains filter state during navigation
-
Sorting:
- Click column headers to sort
- Toggle between ascending/descending
- Visual indicators for active sort
-
Pagination:
- Dynamic items per page based on viewport
- Maintains page state in Redux
- Smooth navigation between pages
-
Form Validation:
- Real-time field validation
- Visual feedback (green/red borders)
- Inline error messages
- Submit button disabled until valid
-
Bulk Operations:
- Excel template download
- Drag-and-drop file upload
- Progress indicators
- Error reporting with line numbers
Responsive Design
// Mobile-first responsive patterns
const ResponsiveStyles = {
// Mobile (< 700px)
mobile: {
layout: 'vertical',
navigation: 'bottom tabs',
forms: 'full width',
modals: 'full screen'
},
// Tablet (700px - 992px)
tablet: {
layout: 'vertical with margins',
navigation: 'top tabs',
forms: 'centered with padding',
modals: 'centered dialog'
},
// Desktop (> 992px)
desktop: {
layout: 'horizontal split view',
navigation: 'top tabs',
forms: 'two-column layout',
modals: 'centered dialog'
}
};Technical Implementation Details
Performance Optimizations
- Lazy Loading:
const DepartmentManager = lazy(() => import('../components/departments/DepartmentManager'));
const DepartmentEditor = lazy(() => import('../components/departments/DepartmentEditor/DepartmentEditor'));- Memoization:
const filteredDepartmentGroup = useMemo(() => {
if (!departmentGroup) return [];
if (!departmentGroupFilter) {
return departmentGroup.filter(d => d.status === activeTab);
}
// Complex filtering logic cached
}, [departmentGroup, departmentGroupFilter, activeTab]);- Debounced Search:
const dispatchFilterInputChange = debounce((value) =>
props.setDepartmentsFilterQuery(value), 1000
);- Session Storage Caching:
const deps = checkExpiredStorageItem<any>(DEPARTMENTS);
if (!deps) {
// Fetch from API
} else {
// Use cached data
}Error Handling
// Comprehensive error handling pattern
const handleDepartmentOperation = async () => {
try {
// Pre-validation
if (!validateInput()) {
throw new ValidationError('Invalid input');
}
// API call
const result = await apiCall();
// Success handling
toast.success('Operation successful');
return result;
} catch (error) {
// Error classification
if (error instanceof ValidationError) {
setValidationErrors(error.errors);
} else if (error.status === 409) {
toast.error('Department already exists');
} else if (error.status === 403) {
toast.error('Permission denied');
} else {
toast.error('An unexpected error occurred');
console.error('Department operation failed:', error);
}
// Error tracking
Monitoring.logEvent('department_operation_error', error);
}
};Security Considerations
-
Authentication:
- Firebase authentication tokens required
- Token refresh handled automatically
- Unauthorized access redirects to login
-
Authorization:
- Role-based access control (RBAC)
- Permission checks at route level
- UI elements hidden based on permissions
-
Input Validation:
- Client-side validation for UX
- Server-side validation for security
- XSS prevention through React sanitization
- SQL injection prevention via parameterized queries
-
Data Privacy:
- Sensitive data not stored in localStorage
- Session storage cleared on logout
- HTTPS enforced for all API calls
Accessibility Features
// ARIA labels and keyboard navigation
<button
id="btn_save_dept"
aria-label="Save department"
onClick={handleSave}
disabled={!isValid}
tabIndex={0}
>
{t('button.save')}
</button>
// Focus management
useEffect(() => {
if (isModalOpen) {
modalRef.current?.focus();
}
}, [isModalOpen]);
// Screen reader announcements
<div role="alert" aria-live="polite">
{error && <span>{error}</span>}
</div>Testing Considerations
// Component testing patterns
describe('DepartmentEditor', () => {
it('should validate department key format', () => {
const { getByLabelText, getByText } = render(<DepartmentEditor />);
const keyInput = getByLabelText('Department Key');
fireEvent.change(keyInput, { target: { value: 'DEPT@123' } });
fireEvent.blur(keyInput);
expect(getByText('Department key cannot contain special characters')).toBeInTheDocument();
});
it('should handle API errors gracefully', async () => {
mockAPI.createDepartment.mockRejectedValue(new Error('Network error'));
const { getByText } = render(<DepartmentEditor />);
fireEvent.click(getByText('Save'));
await waitFor(() => {
expect(getByText('Failed to create department')).toBeInTheDocument();
});
});
});Dependencies and Packages
Core Dependencies
| Package | Version | Purpose |
|---|---|---|
| react | ^17.0.2 | UI framework |
| redux | ^4.1.2 | State management |
| redux-saga | ^1.2.1 | Side effects management |
| react-redux | ^7.2.6 | React-Redux bindings |
| typesafe-actions | ^5.1.0 | Type-safe Redux actions |
| connected-react-router | ^6.9.2 | Redux router integration |
UI Libraries
| Package | Version | Purpose |
|---|---|---|
| styled-components | ^5.3.3 | CSS-in-JS styling |
| react-select | ^5.2.1 | Enhanced select components |
| react-toastify | ^8.1.0 | Toast notifications |
| react-i18next | ^11.15.1 | Internationalization |
Utility Libraries
| Package | Version | Purpose |
|---|---|---|
| lodash | ^4.17.21 | Utility functions (debounce, cloneDeep) |
| react-id-generator | ^3.0.2 | Unique ID generation |
| xlsx | ^0.17.5 | Excel file processing |
| @loadable/component | ^5.15.2 | Code splitting |
Internal Packages
| Package | Purpose |
|---|---|
| @nimbly-technologies/nimbly-common | Shared types and enums |
| config/baseURL | API endpoint configuration |
| helpers/api | API utility functions |
| utils/monitoring | Error tracking and analytics |
| styles/* | Shared styled components |
Development Dependencies
| Package | Purpose |
|---|---|
| typescript | Type checking |
| @types/react | React type definitions |
| @types/styled-components | Styled-components types |
| eslint | Code linting |
| prettier | Code formatting |
Saga Implementation Details
Department Sagas Deep Dive
The department sagas handle all side effects and asynchronous operations. Here’s a detailed breakdown of key saga implementations:
Fetch Departments Saga
export function* fetchDepartments() {
try {
yield put(actions.setLoading(true));
const authToken = yield call(API.getFirebaseToken);
const options = {
method: 'GET',
headers: {
Authorization: authToken,
},
};
// Check session storage cache first
const deps = checkExpiredStorageItem<any>(DEPARTMENTS);
if (!deps) {
// Fetch fresh data from API
const fetchDepartmetsURL = `${apiURL}/departments`;
const request = () => fetch(fetchDepartmetsURL, options);
const response = yield call(request);
if (response && response.status === 200) {
const responseData = yield response.json();
// Cache the response
setToSession(responseData, DEPARTMENTS);
// Transform array to mapped object
const mappingData: { [key: string]: Department } = {};
responseData.data.forEach((department: Department) => {
const departmentKey: string = department.departmentID;
if (!mappingData.hasOwnProperty(departmentKey)) {
mappingData[departmentKey] = department;
}
});
yield put(actions.fetchDepartments.success({ data: mappingData }));
return mappingData;
}
} else {
// Use cached data
const responseData = deps;
const mappingData = transformDepartmentData(responseData.data);
yield put(actions.fetchDepartments.success({ data: mappingData }));
return mappingData;
}
} catch (e) {
yield put(actions.fetchDepartments.failure({ error: 'Failed to Fetch Departments' }));
return null;
}
}Paginated Departments Saga
export function* fetchPaginateDepartments(action: ReturnType<typeof actions.fetchPaginateDepartments.request>) {
try {
const authToken = yield getToken();
const state: RootState = yield select(getState);
const departmentState = state.departments;
// Build sort direction based on current sort field
let sortDirections = 'asc';
switch (departmentState.sortBy) {
case 'name':
sortDirections = departmentState.name;
break;
case 'key':
sortDirections = departmentState.key;
break;
case 'description':
sortDirections = departmentState.description;
break;
case 'email':
sortDirections = departmentState.email;
break;
}
// Build query parameters
const query = {
search: departmentState.filterQuery || '',
page: departmentState.pageIndex,
sortFields: departmentState.sortBy === 'key' ? 'departmentID' : departmentState.sortBy,
sortDirections,
limit: action.payload.limit,
status: departmentState.selectedTab === 'disabled' ? 'disabled' : 'active'
};
const queryString = buildQueryString(query);
const fetchURL = `${apiURL}/departments/paginate?${queryString}`;
const response = yield call(fetch, fetchURL, {
method: 'GET',
headers: { Authorization: authToken }
});
if (response && response.status === 200) {
const responseData = yield response.json();
const mappedData = mapPaginatedDepartments(responseData.data.docs);
yield put(actions.fetchPaginateDepartments.success({
data: mappedData,
total: responseData.data.totalDocs
}));
}
} catch (e) {
handleSagaError(e, 'fetchPaginateDepartments');
}
}Department Group Saga Implementation
Create Department Group Saga
function* createDepartmentGroup(action: ReturnType<typeof actions.createDepartmentGroup.request>) {
try {
// Get current department groups from state
const departmentGroup: DepartmentGroup[] = yield select(departmentGroupSelector) || [];
const departmentGroupList: DepartmentGroup[] = cloneDeep(departmentGroup);
const body = action.payload.data;
// API call to create group
const authToken = yield API.getFirebaseToken();
const url = `${apiURL}/departments/group`;
const response = yield call(API.post, url, authToken, body);
const responseData = yield call(response.json.bind(response));
if (response && response.status === APIResponse.CODE.SUCCESS) {
// Add new group to list
const groupDetail: DepartmentGroup = responseData.data;
groupDetail.ID = responseData.data.ID;
departmentGroupList.push(groupDetail);
// Update state and navigate
yield put(actions.createDepartmentGroup.success({ newData: departmentGroupList }));
toast.success(i18n.t('message.departmentGroupPage.create.success'));
yield delay(1000); // Allow toast to be visible
yield put(push('/admin/departmentsgroup/' + groupDetail.ID));
} else {
handleGroupCreationError(response, responseData);
}
} catch (e) {
yield call(handleSagaError, e, 'createDepartmentGroup');
}
}Component Props and Interfaces
DepartmentManager Props
interface DepartmentManagerProps {
// Redux State
auth: FirebaseAuth;
profile: UserProfile;
isLoading: boolean;
isShowDepartmentBulkUpload: boolean;
departments: { [key: string]: Department };
users: { [key: string]: User };
access: PermissionSet;
activeTab: 'published' | 'disabled';
filterQuery: string;
sortBy: string;
pageIndex: number;
totalDepartments: number;
departmentSchedule: Schedule[];
organization: Organization;
// Redux Actions
fetchDepartments: (payload?: void) => void;
fetchUsers: (payload?: void) => void;
clearDepartmentsFilterQuery: () => void;
setDepartmentsFilterQuery: (text: string) => void;
setShowDepartmentBulkModal: (show: boolean) => void;
push: (path: string) => void;
setDimissDepartmentDeleteModal: () => void;
setSelectedTab: (tab: string) => void;
fetchPaginateDepartments: (payload: { limit: number }) => void;
setPageIndex: (index: number) => void;
fetchDepartmentSchedule: () => void;
fetchDepartmentIndexById: (payload: { deptID: string }) => void;
}DepartmentEditor Props
interface DepartmentEditorProps {
// Data Props
departments: { [key: string]: Department } | null;
users: { [key: string]: User };
sites: { [key: string]: Site };
questionnaires: { [key: string]: Questionnaire };
deptData: DepartmentIndex | null;
deptDataWithRoles: DepartmentIndexUserInSite | null;
// State Props
isLoading: boolean;
isSuccess: boolean;
apiError: string | null;
errorData: string[] | null;
organization: string;
pathname: string;
// Feature Flags
featureAccessIssueEscalation: boolean;
tutorial: TutorialState;
// Actions
fetchDeptById: (payload: { deptID: string }) => void;
fetchDeptWithRolesById: (payload: { deptID: string }) => void;
onCreateDepartment: (payload: { data: DepartmentIndex }) => void;
onUpdateDepartment: (payload: { deptID: string; data: DepartmentIndex; assignSupervisor: any[] }) => void;
clearDepartmentIndex: () => void;
clearDepartmentsByRoles: () => void;
fetchSites: () => void;
fetchDepartments: () => void;
fetchUsers: () => void;
setShowModal: (show: boolean) => void;
setIsSuccess: (success: boolean) => void;
}DepartmentGroupManager Props
interface DepartmentGroupManagerProps {
// State
activeTab: 'active' | 'disabled';
departmentGroup: DepartmentGroup[] | null;
access: PermissionSet;
isDepartmentGroup: boolean;
deptGroupFilter: string;
userRole: string;
organization: Organization;
// Actions
fetchDepartmentGroup: () => void;
fetchUsers: () => void;
clearDepartmentsGroupFilter: (text: string) => void;
setDepartmentsGroupFilter: (text: string) => void;
setActiveTab: (tab: ListTabOptions) => void;
}Advanced Business Logic
Escalation Time Calculation Engine
The escalation system includes sophisticated logic for calculating and managing time-based escalations:
class EscalationEngine {
private escalationConfig: { [level: number]: number | null };
private currentTime: Date;
constructor(config: { [level: number]: number | null }) {
this.escalationConfig = config;
this.currentTime = new Date();
}
// Calculate when an issue should be escalated
calculateEscalationTime(issueCreatedAt: Date, currentLevel: number): Date | null {
const hoursToEscalate = this.escalationConfig[currentLevel];
if (hoursToEscalate === null || hoursToEscalate === -1) {
return null; // No escalation
}
const escalationTime = new Date(issueCreatedAt);
escalationTime.setHours(escalationTime.getHours() + hoursToEscalate);
return escalationTime;
}
// Determine if escalation should occur
shouldEscalate(issueCreatedAt: Date, currentLevel: number): boolean {
const escalationTime = this.calculateEscalationTime(issueCreatedAt, currentLevel);
if (!escalationTime) return false;
return this.currentTime >= escalationTime;
}
// Get next escalation level
getNextLevel(currentLevel: number): number | null {
const levels = Object.keys(this.escalationConfig)
.map(Number)
.sort((a, b) => a - b);
const currentIndex = levels.indexOf(currentLevel);
if (currentIndex === -1 || currentIndex === levels.length - 1) {
return null; // No next level
}
return levels[currentIndex + 1];
}
// Validate escalation configuration
validateConfig(): ValidationResult {
const levels = Object.keys(this.escalationConfig).map(Number).sort((a, b) => a - b);
const errors: string[] = [];
// Check for gaps in levels
for (let i = 1; i < levels.length; i++) {
if (levels[i] !== levels[i - 1] + 1) {
errors.push(`Gap found between level ${levels[i - 1]} and ${levels[i]}`);
}
}
// Check for invalid escalation chains
let foundNoEscalation = false;
for (const level of levels) {
if (foundNoEscalation && this.escalationConfig[level] !== null) {
errors.push(`Cannot have escalation after "No Escalation" at level ${level}`);
}
if (this.escalationConfig[level] === -1) {
foundNoEscalation = true;
}
}
return {
valid: errors.length === 0,
errors
};
}
}Department Assignment Algorithm
The system uses a sophisticated algorithm for managing department assignments:
class DepartmentAssignmentManager {
private departments: Map<string, Department>;
private users: Map<string, User>;
private sites: Map<string, Site>;
// Assign user to department with conflict resolution
assignUserToDepartment(
userId: string,
departmentId: string,
level: number
): AssignmentResult {
// Check if user exists
const user = this.users.get(userId);
if (!user) {
return { success: false, error: 'User not found' };
}
// Check if user is already assigned to another department at same level
const existingAssignment = this.findUserDepartmentAssignment(userId, level);
if (existingAssignment && existingAssignment.departmentId !== departmentId) {
return {
success: false,
error: `User already assigned to ${existingAssignment.departmentName} at level ${level}`,
conflict: existingAssignment
};
}
// Perform assignment
this.departments.get(departmentId)?.users.push({ uid: userId, level });
return { success: true };
}
// Bulk assign sites to department
bulkAssignSites(departmentId: string, siteIds: string[]): BulkAssignmentResult {
const results: AssignmentResult[] = [];
const department = this.departments.get(departmentId);
if (!department) {
return {
success: false,
error: 'Department not found',
results: []
};
}
for (const siteId of siteIds) {
const site = this.sites.get(siteId);
if (!site) {
results.push({
siteId,
success: false,
error: 'Site not found'
});
continue;
}
// Check for circular dependencies
if (this.wouldCreateCircularDependency(departmentId, siteId)) {
results.push({
siteId,
success: false,
error: 'Would create circular dependency'
});
continue;
}
// Assign site
department.sites.push({ id: siteId });
results.push({ siteId, success: true });
}
return {
success: results.every(r => r.success),
results
};
}
// Check for circular dependencies in department-site relationships
private wouldCreateCircularDependency(
departmentId: string,
siteId: string
): boolean {
// Implementation of circular dependency detection
// using depth-first search algorithm
const visited = new Set<string>();
const stack = new Set<string>();
const hasCycle = (deptId: string): boolean => {
visited.add(deptId);
stack.add(deptId);
const dept = this.departments.get(deptId);
if (!dept) return false;
for (const site of dept.sites) {
// Check site's departments
const siteDepts = this.getSiteDepartments(site.id);
for (const siteDept of siteDepts) {
if (!visited.has(siteDept)) {
if (hasCycle(siteDept)) return true;
} else if (stack.has(siteDept)) {
return true; // Cycle detected
}
}
}
stack.delete(deptId);
return false;
};
return hasCycle(departmentId);
}
}Validation Engine
Comprehensive validation engine for department operations:
class DepartmentValidationEngine {
private validators: Map<string, ValidationRule>;
constructor() {
this.initializeValidators();
}
private initializeValidators() {
this.validators = new Map([
['departmentName', {
validate: (value: string) => {
if (!value || value.trim().length === 0) {
return { valid: false, error: 'Department name is required' };
}
if (value.length < 3) {
return { valid: false, error: 'Department name must be at least 3 characters' };
}
if (value.length > 100) {
return { valid: false, error: 'Department name must be less than 100 characters' };
}
return { valid: true };
}
}],
['departmentKey', {
validate: (value: string) => {
const format = /^[a-zA-Z0-9-]+$/;
if (!value) {
return { valid: false, error: 'Department key is required' };
}
if (!format.test(value)) {
return { valid: false, error: 'Department key can only contain letters, numbers, and hyphens' };
}
if (value.length < 2) {
return { valid: false, error: 'Department key must be at least 2 characters' };
}
if (value.length > 50) {
return { valid: false, error: 'Department key must be less than 50 characters' };
}
return { valid: true };
}
}],
['email', {
validate: (value: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!value) {
return { valid: false, error: 'Email is required' };
}
if (!emailRegex.test(value)) {
return { valid: false, error: 'Invalid email format' };
}
return { valid: true };
}
}],
['userAssignments', {
validate: (users: Array<{ uid: string; level: number }>) => {
if (!users || users.length === 0) {
return { valid: false, error: 'At least one user must be assigned' };
}
// Check for duplicate assignments
const userLevels = new Map<string, number[]>();
for (const user of users) {
if (!userLevels.has(user.uid)) {
userLevels.set(user.uid, []);
}
userLevels.get(user.uid)!.push(user.level);
}
for (const [uid, levels] of userLevels.entries()) {
if (levels.length !== new Set(levels).size) {
return {
valid: false,
error: `User ${uid} is assigned to the same level multiple times`
};
}
}
return { valid: true };
}
}]
]);
}
// Validate entire department object
validateDepartment(department: Partial<DepartmentIndex>): ValidationResult {
const errors: ValidationError[] = [];
// Validate each field
for (const [field, validator] of this.validators.entries()) {
const value = department[field as keyof DepartmentIndex];
const result = validator.validate(value);
if (!result.valid) {
errors.push({
field,
message: result.error,
severity: 'error'
});
}
}
// Custom validations
errors.push(...this.performCustomValidations(department));
return {
valid: errors.length === 0,
errors
};
}
private performCustomValidations(department: Partial<DepartmentIndex>): ValidationError[] {
const errors: ValidationError[] = [];
// Validate escalation configuration
if (department.escalateTime) {
const escalationEngine = new EscalationEngine(department.escalateTime);
const escalationValidation = escalationEngine.validateConfig();
if (!escalationValidation.valid) {
errors.push(...escalationValidation.errors.map(error => ({
field: 'escalateTime',
message: error,
severity: 'error' as const
})));
}
}
// Validate issue owner is in assigned users
if (department.defaultIssueOwner && department.users) {
const isOwnerAssigned = department.users.some(
user => user.uid === department.defaultIssueOwner
);
if (!isOwnerAssigned) {
errors.push({
field: 'defaultIssueOwner',
message: 'Default issue owner must be an assigned user',
severity: 'warning'
});
}
}
return errors;
}
}UI/UX Patterns and Interactions
Advanced Search and Filter Implementation
The department module implements a sophisticated search and filter system:
class DepartmentSearchEngine {
private searchIndex: Map<string, SearchDocument>;
private filters: FilterSet;
constructor() {
this.searchIndex = new Map();
this.filters = new FilterSet();
}
// Build search index for fast searching
buildSearchIndex(departments: Department[]) {
departments.forEach(dept => {
const searchDoc: SearchDocument = {
id: dept.departmentID,
tokens: this.tokenize([
dept.name,
dept.description,
dept.email,
dept.departmentID
].join(' ')),
metadata: {
status: dept.status,
createdAt: dept.createdAt,
userCount: dept.users?.length || 0,
siteCount: dept.sites?.length || 0
}
};
this.searchIndex.set(dept.departmentID, searchDoc);
});
}
// Perform search with relevance scoring
search(query: string, filters?: FilterCriteria): SearchResult[] {
const queryTokens = this.tokenize(query.toLowerCase());
const results: SearchResult[] = [];
for (const [id, doc] of this.searchIndex.entries()) {
// Apply filters first
if (filters && !this.matchesFilters(doc, filters)) {
continue;
}
// Calculate relevance score
const score = this.calculateRelevanceScore(queryTokens, doc.tokens);
if (score > 0) {
results.push({
departmentId: id,
score,
highlights: this.generateHighlights(queryTokens, doc)
});
}
}
// Sort by relevance score
return results.sort((a, b) => b.score - a.score);
}
private calculateRelevanceScore(queryTokens: string[], docTokens: string[]): number {
let score = 0;
const docTokenSet = new Set(docTokens);
for (const queryToken of queryTokens) {
if (docTokenSet.has(queryToken)) {
score += 1; // Exact match
} else {
// Fuzzy matching
for (const docToken of docTokens) {
const similarity = this.calculateSimilarity(queryToken, docToken);
if (similarity > 0.8) {
score += similarity;
break;
}
}
}
}
return score;
}
private calculateSimilarity(str1: string, str2: string): number {
// Levenshtein distance implementation
const matrix: number[][] = [];
for (let i = 0; i <= str2.length; i++) {
matrix[i] = [i];
}
for (let j = 0; j <= str1.length; j++) {
matrix[0][j] = j;
}
for (let i = 1; i <= str2.length; i++) {
for (let j = 1; j <= str1.length; j++) {
if (str2.charAt(i - 1) === str1.charAt(j - 1)) {
matrix[i][j] = matrix[i - 1][j - 1];
} else {
matrix[i][j] = Math.min(
matrix[i - 1][j - 1] + 1,
matrix[i][j - 1] + 1,
matrix[i - 1][j] + 1
);
}
}
}
const distance = matrix[str2.length][str1.length];
return 1 - (distance / Math.max(str1.length, str2.length));
}
}Drag and Drop Implementation
The department group editor implements drag-and-drop functionality:
class DragDropManager {
private draggedItem: DragItem | null = null;
private dropZones: Map<string, DropZone> = new Map();
initializeDragDrop() {
// Set up drag event listeners
document.addEventListener('dragstart', this.handleDragStart.bind(this));
document.addEventListener('dragend', this.handleDragEnd.bind(this));
document.addEventListener('dragover', this.handleDragOver.bind(this));
document.addEventListener('drop', this.handleDrop.bind(this));
}
private handleDragStart(e: DragEvent) {
const target = e.target as HTMLElement;
if (target.dataset.draggable === 'department') {
this.draggedItem = {
type: 'department',
id: target.dataset.departmentId!,
data: JSON.parse(target.dataset.departmentData!)
};
// Visual feedback
target.classList.add('dragging');
e.dataTransfer!.effectAllowed = 'move';
// Store drag data
e.dataTransfer!.setData('text/plain', JSON.stringify(this.draggedItem));
}
}
private handleDragOver(e: DragEvent) {
e.preventDefault();
const dropZone = this.findDropZone(e.target as HTMLElement);
if (dropZone && this.canDrop(this.draggedItem!, dropZone)) {
e.dataTransfer!.dropEffect = 'move';
dropZone.element.classList.add('drag-over');
} else {
e.dataTransfer!.dropEffect = 'none';
}
}
private handleDrop(e: DragEvent) {
e.preventDefault();
const dropZone = this.findDropZone(e.target as HTMLElement);
if (dropZone && this.draggedItem) {
const success = this.processDrop(this.draggedItem, dropZone);
if (success) {
// Animate the drop
this.animateDrop(this.draggedItem, dropZone);
// Update the UI
this.updateUI(this.draggedItem, dropZone);
}
}
this.cleanup();
}
private canDrop(item: DragItem, zone: DropZone): boolean {
// Validate drop rules
if (item.type === 'department' && zone.type === 'departmentGroup') {
// Check if department is already in group
const group = zone.data as DepartmentGroup;
return !group.departments.some(d => d.id === item.id);
}
return false;
}
private animateDrop(item: DragItem, zone: DropZone) {
const itemElement = document.querySelector(`[data-department-id="${item.id}"]`);
const zoneRect = zone.element.getBoundingClientRect();
if (itemElement) {
// Create clone for animation
const clone = itemElement.cloneNode(true) as HTMLElement;
clone.style.position = 'fixed';
clone.style.transition = 'all 0.3s ease-out';
clone.style.zIndex = '9999';
document.body.appendChild(clone);
// Animate to drop zone
requestAnimationFrame(() => {
clone.style.left = `${zoneRect.left}px`;
clone.style.top = `${zoneRect.top}px`;
clone.style.opacity = '0';
clone.style.transform = 'scale(0.8)';
});
// Remove clone after animation
setTimeout(() => clone.remove(), 300);
}
}
}Real-time Validation Feedback
The module provides sophisticated real-time validation feedback:
class ValidationFeedbackManager {
private validators: Map<string, FieldValidator>;
private debounceTimers: Map<string, NodeJS.Timeout>;
constructor() {
this.validators = new Map();
this.debounceTimers = new Map();
}
// Register field validator
registerValidator(fieldName: string, validator: FieldValidator) {
this.validators.set(fieldName, validator);
}
// Validate field with debounce
validateField(fieldName: string, value: any, immediate = false) {
// Clear existing timer
if (this.debounceTimers.has(fieldName)) {
clearTimeout(this.debounceTimers.get(fieldName)!);
}
const validate = () => {
const validator = this.validators.get(fieldName);
if (!validator) return;
const result = validator.validate(value);
this.updateFieldUI(fieldName, result);
// Trigger dependent validations
if (validator.dependencies) {
validator.dependencies.forEach(dep => {
const depValue = this.getFieldValue(dep);
this.validateField(dep, depValue, true);
});
}
};
if (immediate) {
validate();
} else {
// Debounce validation
const timer = setTimeout(validate, validator.debounceMs || 500);
this.debounceTimers.set(fieldName, timer);
}
}
private updateFieldUI(fieldName: string, result: ValidationResult) {
const field = document.querySelector(`[data-field="${fieldName}"]`) as HTMLElement;
if (!field) return;
const input = field.querySelector('input, select, textarea') as HTMLElement;
const errorContainer = field.querySelector('.error-message') as HTMLElement;
if (result.valid) {
// Success state
input.classList.remove('error');
input.classList.add('success');
if (errorContainer) {
errorContainer.style.opacity = '0';
setTimeout(() => {
errorContainer.textContent = '';
}, 200);
}
// Show success icon
this.showSuccessIcon(field);
} else {
// Error state
input.classList.remove('success');
input.classList.add('error');
if (errorContainer) {
errorContainer.textContent = result.error || '';
errorContainer.style.opacity = '1';
}
// Shake animation
this.shakeField(field);
}
}
private showSuccessIcon(field: HTMLElement) {
const existingIcon = field.querySelector('.success-icon');
if (existingIcon) return;
const icon = document.createElement('span');
icon.className = 'success-icon';
icon.innerHTML = '✓';
icon.style.cssText = `
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
color: #3bd070;
font-weight: bold;
opacity: 0;
transition: opacity 0.3s ease;
`;
field.appendChild(icon);
requestAnimationFrame(() => {
icon.style.opacity = '1';
});
}
private shakeField(field: HTMLElement) {
field.classList.add('shake');
setTimeout(() => field.classList.remove('shake'), 500);
}
}Error Handling Scenarios
Comprehensive Error Handler
class DepartmentErrorHandler {
private errorMappings: Map<string, ErrorHandler>;
private errorLog: ErrorLogEntry[];
constructor() {
this.initializeErrorMappings();
this.errorLog = [];
}
private initializeErrorMappings() {
this.errorMappings = new Map([
['NETWORK_ERROR', {
handler: (error: Error) => {
// Check if offline
if (!navigator.onLine) {
return {
message: 'You are offline. Please check your internet connection.',
severity: 'warning',
actions: [
{
label: 'Retry',
action: () => window.location.reload()
}
]
};
}
// Network timeout
if (error.message.includes('timeout')) {
return {
message: 'The request timed out. Please try again.',
severity: 'error',
actions: [
{
label: 'Retry',
action: () => this.retryLastAction()
}
]
};
}
return {
message: 'A network error occurred. Please try again.',
severity: 'error'
};
}
}],
['VALIDATION_ERROR', {
handler: (error: ValidationError) => {
const fieldErrors = error.errors.map(e => `${e.field}: ${e.message}`).join('\n');
return {
message: 'Please fix the following errors:\n' + fieldErrors,
severity: 'error',
persistent: true
};
}
}],
['PERMISSION_ERROR', {
handler: (error: Error) => {
return {
message: 'You do not have permission to perform this action.',
severity: 'error',
actions: [
{
label: 'Request Access',
action: () => this.requestAccess()
}
]
};
}
}],
['CONFLICT_ERROR', {
handler: (error: ConflictError) => {
if (error.type === 'DUPLICATE_KEY') {
return {
message: `A department with key "${error.conflictingValue}" already exists.`,
severity: 'error',
actions: [
{
label: 'Choose Different Key',
action: () => this.focusField('departmentKey')
}
]
};
}
return {
message: 'A conflict occurred. Please refresh and try again.',
severity: 'error'
};
}
}]
]);
}
handleError(error: Error, context?: ErrorContext): ErrorResponse {
// Log error
this.logError(error, context);
// Determine error type
const errorType = this.classifyError(error);
// Get appropriate handler
const handler = this.errorMappings.get(errorType);
if (handler) {
const response = handler.handler(error);
// Show error to user
this.showError(response);
// Track error
this.trackError(error, errorType, context);
return response;
}
// Fallback error handling
return this.handleUnknownError(error);
}
private classifyError(error: Error): string {
if (error.name === 'NetworkError' || error.message.includes('fetch')) {
return 'NETWORK_ERROR';
}
if (error.name === 'ValidationError') {
return 'VALIDATION_ERROR';
}
if (error.message.includes('403') || error.message.includes('unauthorized')) {
return 'PERMISSION_ERROR';
}
if (error.message.includes('409') || error.message.includes('conflict')) {
return 'CONFLICT_ERROR';
}
return 'UNKNOWN_ERROR';
}
private showError(response: ErrorResponse) {
if (response.severity === 'warning') {
toast.warning(response.message, {
autoClose: response.persistent ? false : 5000,
closeButton: true
});
} else {
toast.error(response.message, {
autoClose: response.persistent ? false : 5000,
closeButton: true,
action: response.actions?.[0] ? {
label: response.actions[0].label,
onClick: response.actions[0].action
} : undefined
});
}
}
private async trackError(error: Error, type: string, context?: ErrorContext) {
try {
await Monitoring.logEvent('department_error', {
error_type: type,
error_message: error.message,
error_stack: error.stack,
context: context,
timestamp: new Date().toISOString(),
user_agent: navigator.userAgent,
url: window.location.href
});
} catch (trackingError) {
console.error('Failed to track error:', trackingError);
}
}
}This documentation provides a comprehensive technical overview of the Department Module in the Nimbly audit-admin platform. The module demonstrates a well-architected system with clear separation of concerns, robust state management, and thoughtful user experience design. The implementation follows React best practices and maintains consistency with the broader application architecture.
For additional information or specific implementation details, please refer to the source code files referenced throughout this documentation via the GitHub links provided.