Overview
The General Settings module in the Nimbly Audit Admin application provides a centralized configuration interface for organization-wide settings. This module allows administrators to configure various aspects of the system including timezone settings, report configurations, issue tracking workflows, Learning Management System (LMS) parameters, site-wide preferences, and custom branding.
The module is built using React with TypeScript, leveraging Redux for state management and Firebase Realtime Database for data persistence. It implements a tab-based interface with role-based access control to ensure appropriate permission levels for different configuration areas.
Architecture
High-Level Architecture
graph TB subgraph "Frontend Layer" SP[Settings Page] SM[SettingsManager] SC[SettingsManagerContainer] TABS[Tab Components] end subgraph "State Management" REDUX[Redux Store] ACTIONS[Actions] REDUCERS[Reducers] SELECTORS[Selectors] end subgraph "Data Layer" FIREBASE[Firebase Realtime DB] API[REST API] SERVICES[Service Layer] end SP --> SC SC --> SM SM --> TABS TABS --> ACTIONS ACTIONS --> REDUCERS REDUCERS --> REDUX REDUX --> SELECTORS SELECTORS --> TABS SERVICES --> FIREBASE SERVICES --> API ACTIONS --> SERVICES
Component Hierarchy
The settings module follows a hierarchical component structure:
-
Settings Page (src/pages/settings.js)
- Entry point for the settings module
- Wraps content in Layout component
- Implements lazy loading for performance optimization
- Includes LeavePageConfirmationModal for unsaved changes protection
-
SettingsManagerContainer (src/components/settings/SettingsManager/SettingsManagerContainer.tsx)
- Redux container component
- Maps state and dispatch to props
- Wraps SettingsManager with SettingsReportProvider context
-
SettingsManager (src/components/settings/SettingsManager/SettingsManager.tsx)
- Main component implementing tab navigation
- Manages active tab state
- Renders appropriate tab content based on selection
- Handles permission checks for tab visibility
State Management Architecture
The module uses Redux for state management with the following structure:
graph LR subgraph "Redux Store" ORG[Organization State] USER[User State] SETTINGS[Settings State] LMS[LMS State] ISSUE[Issue Tracker State] end subgraph "Actions" OA[Organization Actions] UA[User Actions] SA[Settings Actions] LA[LMS Actions] IA[Issue Actions] end subgraph "Reducers" OR[Organization Reducer] UR[User Reducer] SR[Settings Reducer] LR[LMS Reducer] IR[Issue Reducer] end OA --> OR --> ORG UA --> UR --> USER SA --> SR --> SETTINGS LA --> LR --> LMS IA --> IR --> ISSUE
Module Structure
File Organization
src/
├── components/
│ └── settings/
│ └── SettingsManager/
│ ├── SettingsManager.tsx
│ ├── SettingsManagerContainer.tsx
│ ├── IssueTrackerSettings/
│ │ ├── IssueTrackerSettingsPage.tsx
│ │ ├── components/
│ │ └── modals/
│ ├── SettingsReport/
│ │ ├── SettingsReport.tsx
│ │ └── components/
│ ├── LMSSettings/
│ │ └── LMSSettings.tsx
│ └── CustomizationOrgs/
│ └── CustomizationOrgs.tsx
├── services/
│ ├── OrganizationService.ts
│ ├── UserService.ts
│ ├── LMSService.ts
│ └── IssueTrackerService.ts
├── store/
│ ├── actions/
│ ├── reducers/
│ └── selectors/
└── utils/
└── settings/
Core Dependencies
The settings module relies on several key dependencies:
- React & React-DOM: Core UI framework
- Redux & React-Redux: State management
- Firebase: Real-time database for settings persistence
- React Hook Form: Form state management and validation
- Material-UI: UI component library
- React Router: Navigation and routing
- Moment.js & Moment-Timezone: Date/time handling with timezone support
General Settings Tab
The General Settings tab provides core organizational configuration options that affect system-wide behavior. This tab is accessible to users with appropriate permissions and contains four main configuration sections.
Component Structure
Main Component: SettingsManager.tsx (General tab section)
Location: src/components/settings/SettingsManager/SettingsManager.tsx:397-662
Configuration Sections
1. Timezone Configuration
The timezone setting affects all time-based calculations and displays throughout the system.
Component: TimezoneSelect
Location: src/components/global/Common/TimezoneSelect.tsx
Implementation Details:
const TimezoneSelect = ({
value,
onChange,
disabled = false
}: TimezoneSelectProps) => {
// Fetches list of timezones from moment-timezone
const timezones = moment.tz.names();
// Formats timezone for display (e.g., "America/New_York (UTC-05:00)")
const formatTimezone = (tz: string) => {
const offset = moment.tz(tz).format('Z');
return `${tz} (UTC${offset})`;
};
return (
<Select
value={value}
onChange={onChange}
disabled={disabled}
fullWidth
>
{timezones.map(tz => (
<MenuItem key={tz} value={tz}>
{formatTimezone(tz)}
</MenuItem>
))}
</Select>
);
};Data Storage:
- Firebase Path:
/organization/{organizationId}/timezone - Format: IANA timezone identifier (e.g., “America/New_York”)
- Default: “UTC”
Business Logic:
- Timezone changes affect all timestamp displays in reports
- Historical data remains in original timezone
- Future scheduled tasks adjust to new timezone
- User preferences can override organization timezone
2. Report Period Configuration
Controls the default time periods used for report generation across the system.
Configuration Options:
-
Period Unit:
- Week (7 days)
- Month (30 days)
- Year (365 days)
-
Period Start:
- For Week: Day of week (0-6, where 0 = Sunday)
- For Month: Day of month (1-31)
- For Year: Day of year (1-365)
Implementation:
interface ReportPeriodConfig {
periodUnit: 'week' | 'month' | 'year';
periodStart: number;
}
const handlePeriodChange = (config: ReportPeriodConfig) => {
// Validate period start based on unit
const isValid = validatePeriodStart(config);
if (!isValid) {
showError('Invalid period start value');
return;
}
// Update organization schedule
updateOrganizationSchedule({
periodUnit: config.periodUnit,
periodStart: config.periodStart
});
};Validation Rules:
- Week: periodStart must be 0-6
- Month: periodStart must be 1-31 (automatically adjusts for months with fewer days)
- Year: periodStart must be 1-365
Data Storage:
- Firebase Path:
/organization/{organizationId}/schedule - Structure:
{ "periodUnit": "month", "periodStart": 1 }
3. Blocked Tab Visibility
Controls whether the “Blocked” tab appears in various parts of the application.
Implementation:
const [displayBlockedTab, setDisplayBlockedTab] = useState(
organization?.displayBlockedTab ?? true
);
const handleBlockedTabToggle = async (event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.checked;
setDisplayBlockedTab(newValue);
try {
await updateOrganization({
displayBlockedTab: newValue
});
showSuccess('Blocked tab visibility updated');
} catch (error) {
showError('Failed to update blocked tab visibility');
setDisplayBlockedTab(!newValue); // Revert on error
}
};Business Impact:
- Affects visibility in issue lists, reports, and dashboards
- Does not delete blocked items, only hides the UI
- Can be overridden by user-level permissions
4. Password Policy Configuration
Defines organization-wide password complexity requirements.
Policy Levels:
-
Simple (Level 1):
- Minimum 8 characters
- At least one letter
- At least one number
-
Medium (Level 2):
- Minimum 10 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
-
Complex (Level 3):
- Minimum 12 characters
- At least two uppercase letters
- At least two lowercase letters
- At least two numbers
- At least two special characters
- Cannot contain username
- Cannot contain common patterns
Implementation:
enum PasswordPolicyLevel {
SIMPLE = 1,
MEDIUM = 2,
COMPLEX = 3
}
interface PasswordPolicy {
level: PasswordPolicyLevel;
minLength: number;
requireUppercase: boolean;
requireLowercase: boolean;
requireNumbers: boolean;
requireSpecialChars: boolean;
minUppercase?: number;
minLowercase?: number;
minNumbers?: number;
minSpecialChars?: number;
preventUsername?: boolean;
preventCommonPatterns?: boolean;
}
const passwordPolicies: Record<PasswordPolicyLevel, PasswordPolicy> = {
[PasswordPolicyLevel.SIMPLE]: {
level: PasswordPolicyLevel.SIMPLE,
minLength: 8,
requireUppercase: false,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: false
},
[PasswordPolicyLevel.MEDIUM]: {
level: PasswordPolicyLevel.MEDIUM,
minLength: 10,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
minUppercase: 1,
minLowercase: 1,
minNumbers: 1,
minSpecialChars: 1
},
[PasswordPolicyLevel.COMPLEX]: {
level: PasswordPolicyLevel.COMPLEX,
minLength: 12,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
minUppercase: 2,
minLowercase: 2,
minNumbers: 2,
minSpecialChars: 2,
preventUsername: true,
preventCommonPatterns: true
}
};Validation Function:
const validatePassword = (
password: string,
policy: PasswordPolicy,
username?: string
): ValidationResult => {
const errors: string[] = [];
// Length check
if (password.length < policy.minLength) {
errors.push(`Password must be at least ${policy.minLength} characters`);
}
// Character type checks
const upperCount = (password.match(/[A-Z]/g) || []).length;
const lowerCount = (password.match(/[a-z]/g) || []).length;
const numberCount = (password.match(/[0-9]/g) || []).length;
const specialCount = (password.match(/[^A-Za-z0-9]/g) || []).length;
if (policy.requireUppercase && upperCount < (policy.minUppercase || 1)) {
errors.push(`Password must contain at least ${policy.minUppercase || 1} uppercase letter(s)`);
}
if (policy.requireLowercase && lowerCount < (policy.minLowercase || 1)) {
errors.push(`Password must contain at least ${policy.minLowercase || 1} lowercase letter(s)`);
}
if (policy.requireNumbers && numberCount < (policy.minNumbers || 1)) {
errors.push(`Password must contain at least ${policy.minNumbers || 1} number(s)`);
}
if (policy.requireSpecialChars && specialCount < (policy.minSpecialChars || 1)) {
errors.push(`Password must contain at least ${policy.minSpecialChars || 1} special character(s)`);
}
// Username check
if (policy.preventUsername && username && password.toLowerCase().includes(username.toLowerCase())) {
errors.push('Password cannot contain username');
}
// Common patterns check
if (policy.preventCommonPatterns) {
const commonPatterns = ['123456', 'password', 'qwerty', 'abc123'];
const hasCommonPattern = commonPatterns.some(pattern =>
password.toLowerCase().includes(pattern)
);
if (hasCommonPattern) {
errors.push('Password contains common patterns');
}
}
return {
isValid: errors.length === 0,
errors
};
};Save Mechanism
The General Settings tab implements an optimistic update pattern with rollback on failure:
const saveGeneralSettings = async () => {
setIsSaving(true);
const previousState = { ...currentSettings };
try {
// Update UI optimistically
updateUIState(newSettings);
// Batch update to Firebase
const updates = {
[`/organization/${organizationId}/timezone`]: newSettings.timezone,
[`/organization/${organizationId}/schedule`]: {
periodUnit: newSettings.periodUnit,
periodStart: newSettings.periodStart
},
[`/organization/${organizationId}/displayBlockedTab`]: newSettings.displayBlockedTab,
[`/organization/${organizationId}/passwordPolicy`]: newSettings.passwordPolicy
};
await firebaseDatabase.ref().update(updates);
// Log activity
await logActivity({
action: 'UPDATE_GENERAL_SETTINGS',
details: getDiff(previousState, newSettings)
});
showSuccess('General settings updated successfully');
} catch (error) {
// Rollback on failure
updateUIState(previousState);
showError('Failed to save general settings');
console.error('General settings save error:', error);
} finally {
setIsSaving(false);
}
};Permission Model
Access to General Settings is controlled through a granular permission system:
const generalSettingsPermissions = {
VIEW: 'setting.permission.general.view',
EDIT: 'setting.permission.general.edit',
TIMEZONE_EDIT: 'setting.permission.general.timezone.edit',
REPORT_PERIOD_EDIT: 'setting.permission.general.report_period.edit',
BLOCKED_TAB_EDIT: 'setting.permission.general.blocked_tab.edit',
PASSWORD_POLICY_EDIT: 'setting.permission.general.password_policy.edit'
};
const canEditField = (field: string): boolean => {
const user = getCurrentUser();
const baseEdit = hasPermission(user, generalSettingsPermissions.EDIT);
const fieldPermission = generalSettingsPermissions[`${field.toUpperCase()}_EDIT`];
return baseEdit || hasPermission(user, fieldPermission);
};API Integration
The General Settings tab interacts with several API endpoints:
-
Organization Update API
- Endpoint:
PUT /api/v1/organizations/{organizationId} - Headers:
Authorization: Bearer {token} - Payload:
{ "timezone": "America/New_York", "displayBlockedTab": true, "passwordPolicy": 2 }
- Endpoint:
-
Schedule Update API
- Endpoint:
PUT /api/v1/organizations/{organizationId}/schedule - Headers:
Authorization: Bearer {token} - Payload:
{ "periodUnit": "month", "periodStart": 1 }
- Endpoint:
-
Activity Log API
- Endpoint:
POST /api/v1/activity-logs - Headers:
Authorization: Bearer {token} - Payload:
{ "action": "UPDATE_GENERAL_SETTINGS", "entityType": "organization", "entityId": "{organizationId}", "details": { "changes": { "timezone": { "old": "UTC", "new": "America/New_York" } } } }
- Endpoint:
Issue Tracker Settings Tab
The Issue Tracker Settings tab provides comprehensive configuration options for managing issue tracking workflows, assignment rules, due date automation, and escalation policies. This tab is crucial for organizations to customize how issues flow through the system from creation to resolution.
Component Architecture
Main Component: IssueTrackerSettingsPage
Location: src/components/settings/SettingsManager/IssueTrackerSettings/IssueTrackerSettingsPage.tsx
The Issue Tracker Settings uses a Context-based state management approach:
graph TD subgraph "Issue Tracker Context" ITC[IssueTrackerContext] ITP[IssueTrackerProvider] UIH[useIssueTracker Hook] end subgraph "Components" ITSP[IssueTrackerSettingsPage] DUEDATE[DueDateSettings] ASSIGN[AssignmentSettings] GENERAL[GeneralSettings] APPROVAL[ApprovalSettings] end subgraph "Modals" ADDM[AddDueDateModal] EDITM[EditDueDateModal] DELM[DeleteConfirmationModal] HIER[HierarchyModal] end ITP --> ITC ITSP --> UIH UIH --> ITC ITSP --> DUEDATE ITSP --> ASSIGN ITSP --> GENERAL ITSP --> APPROVAL DUEDATE --> ADDM DUEDATE --> EDITM DUEDATE --> DELM ASSIGN --> HIER
Configuration Sections
1. Due Date Settings
Controls how due dates are assigned to issues, supporting both manual and automatic assignment modes.
Component: DueDateSettings
Location: Components are inline within IssueTrackerSettingsPage
Configuration Modes:
-
Manual Due Date Assignment
- Auditors manually set due dates for each issue
- No automated rules applied
- Provides maximum flexibility
-
Automatic Due Date Assignment
- System automatically calculates due dates based on predefined rules
- Rules are priority-based (P0, P1, P2)
- Can be site-specific or organization-wide
Auto Due Date Rule Structure:
interface AutoDueDateRule {
id: string;
name: string;
sites: Site[]; // Empty array means organization-wide
priorities: {
P0: number; // Days for P0 priority
P1: number; // Days for P1 priority
P2: number; // Days for P2 priority
};
isDefault: boolean;
isActive: boolean;
createdAt: string;
updatedAt: string;
}Rule Management Features:
-
Add New Rule Modal:
const AddDueDateModal = ({ isOpen, onClose, onSave }: AddDueDateModalProps) => { const [formData, setFormData] = useState<DueDateFormData>({ name: '', sites: [], priorities: { P0: 1, P1: 3, P2: 7 }, isDefault: false }); const handleSave = async () => { // Validation if (!formData.name.trim()) { showError('Rule name is required'); return; } if (formData.isDefault && formData.sites.length > 0) { showError('Default rules cannot be site-specific'); return; } // Create rule const newRule = await createAutoDueDateRule({ ...formData, isActive: true, organizationId: currentOrg.id }); onSave(newRule); onClose(); }; }; -
Rule Priority Logic:
- Only one default rule allowed per organization
- Site-specific rules override default rules
- If multiple rules apply to a site, the most specific one is used
- Rule matching hierarchy: Site-specific → Default → Manual
-
Due Date Calculation:
const calculateDueDate = ( issue: Issue, rules: AutoDueDateRule[] ): Date => { const priority = issue.priority || 'P2'; const siteId = issue.siteId; // Find applicable rule let applicableRule = rules.find(rule => rule.isActive && rule.sites.some(site => site.id === siteId) ); // Fallback to default rule if (!applicableRule) { applicableRule = rules.find(rule => rule.isActive && rule.isDefault ); } // Calculate due date if (applicableRule) { const daysToAdd = applicableRule.priorities[priority]; return addBusinessDays(new Date(), daysToAdd); } // No rule found - return null for manual assignment return null; };
Search and Pagination:
interface DueDateListState {
rules: AutoDueDateRule[];
searchQuery: string;
currentPage: number;
pageSize: number;
totalCount: number;
isLoading: boolean;
}
const filteredRules = useMemo(() => {
return rules.filter(rule => {
const searchLower = searchQuery.toLowerCase();
return (
rule.name.toLowerCase().includes(searchLower) ||
rule.sites.some(site =>
site.name.toLowerCase().includes(searchLower)
)
);
});
}, [rules, searchQuery]);2. Assignment Settings
Manages how issues are assigned to users, supporting both manual and automatic assignment based on configurable hierarchy.
Assignment Modes:
-
Manual Assignment
- Issues remain unassigned upon creation
- Managers/admins manually assign to auditors
- Suitable for small teams or specific workflows
-
Automatic Assignment
- System assigns based on hierarchy rules
- Customizable assignment chain
- Fallback mechanisms for missing assignees
Assignment Hierarchy:
Default hierarchy (customizable via drag-and-drop):
enum AssignmentLevel {
QUESTION = 'question',
CATEGORY = 'category',
QUESTIONNAIRE = 'questionnaire',
SITE = 'site',
DEPT_ESCALATION = 'deptEscalation',
DEPARTMENT = 'department',
SCHEDULE = 'schedule',
REPORTER = 'reporter'
}
interface AssignmentHierarchy {
levels: AssignmentLevel[];
enabledLevels: Set<AssignmentLevel>;
}Hierarchy Modal Implementation:
const HierarchyModal = ({
isOpen,
onClose,
hierarchy,
onSave
}: HierarchyModalProps) => {
const [items, setItems] = useState(hierarchy.levels);
const handleDragEnd = (result: DropResult) => {
if (!result.destination) return;
const newItems = Array.from(items);
const [reorderedItem] = newItems.splice(result.source.index, 1);
newItems.splice(result.destination.index, 0, reorderedItem);
setItems(newItems);
};
const hierarchyDescriptions = {
[AssignmentLevel.QUESTION]: 'Assign to question owner',
[AssignmentLevel.CATEGORY]: 'Assign to category owner',
[AssignmentLevel.QUESTIONNAIRE]: 'Assign to questionnaire owner',
[AssignmentLevel.SITE]: 'Assign to site manager',
[AssignmentLevel.DEPT_ESCALATION]: 'Escalate to department head',
[AssignmentLevel.DEPARTMENT]: 'Assign to department default',
[AssignmentLevel.SCHEDULE]: 'Assign to schedule creator',
[AssignmentLevel.REPORTER]: 'Assign to issue reporter'
};
};Assignment Resolution Algorithm:
const resolveAssignee = async (
issue: Issue,
hierarchy: AssignmentHierarchy
): Promise<User | null> => {
for (const level of hierarchy.levels) {
if (!hierarchy.enabledLevels.has(level)) continue;
let assignee: User | null = null;
switch (level) {
case AssignmentLevel.QUESTION:
const question = await getQuestion(issue.questionId);
assignee = question?.assignedTo;
break;
case AssignmentLevel.CATEGORY:
const category = await getCategory(issue.categoryId);
assignee = category?.owner;
break;
case AssignmentLevel.QUESTIONNAIRE:
const questionnaire = await getQuestionnaire(issue.questionnaireId);
assignee = questionnaire?.manager;
break;
case AssignmentLevel.SITE:
const site = await getSite(issue.siteId);
assignee = site?.manager;
break;
case AssignmentLevel.DEPT_ESCALATION:
if (issue.escalationLevel > 0) {
const dept = await getDepartment(issue.departmentId);
assignee = dept?.head;
}
break;
case AssignmentLevel.DEPARTMENT:
const dept = await getDepartment(issue.departmentId);
assignee = dept?.defaultAssignee;
break;
case AssignmentLevel.SCHEDULE:
const schedule = await getSchedule(issue.scheduleId);
assignee = schedule?.creator;
break;
case AssignmentLevel.REPORTER:
assignee = await getUser(issue.reporterId);
break;
}
if (assignee && assignee.isActive) {
return assignee;
}
}
return null; // No assignee found
};3. General Settings
Provides miscellaneous configuration options for issue tracker behavior.
Settings Options:
-
Auditor Can Submit for Approval
- Boolean toggle
- When enabled, auditors can directly submit issues for approval
- When disabled, only managers can submit for approval
-
Enable Auto Escalation
- Boolean toggle
- Automatically escalates issues based on SLA breaches
- Configurable escalation rules and timelines
-
Default Approver Settings
- Configure fallback approvers when primary approver unavailable
- Hierarchy-based or role-based selection
Implementation:
interface GeneralIssueSettings {
auditorCanSubmitForApproval: boolean;
enableAutoEscalation: boolean;
defaultApprover: {
type: 'hierarchy' | 'role' | 'specific';
value: string; // Role ID or User ID
};
escalationRules: EscalationRule[];
}
interface EscalationRule {
id: string;
name: string;
triggerAfterHours: number;
escalateTo: 'manager' | 'department_head' | 'admin';
notifyVia: ('email' | 'sms' | 'in_app')[];
isActive: boolean;
}4. Approval Settings
Manages the approval workflow for issues, including SLA-based auto-approval and rejection mechanisms.
Component: Uses Redux-connected ApprovalSettings component
Location: Integrated within IssueTrackerSettingsPage
Key Features:
-
SLA Configuration
- Define time limits for approval actions
- Separate SLAs for different priority levels
- Business hours vs calendar hours options
-
Auto-Approval Rules
interface AutoApprovalRule { id: string; name: string; conditions: ApprovalCondition[]; slaHours: number; action: 'approve' | 'reject'; includeWeekends: boolean; notifyOnAction: boolean; } interface ApprovalCondition { field: 'priority' | 'category' | 'severity' | 'site'; operator: 'equals' | 'not_equals' | 'in' | 'not_in'; value: string | string[]; } -
Approval Workflow State Machine
stateDiagram-v2 [*] --> Created Created --> Assigned Assigned --> InProgress InProgress --> PendingApproval PendingApproval --> Approved PendingApproval --> Rejected PendingApproval --> AutoApproved PendingApproval --> AutoRejected Rejected --> InProgress Approved --> Closed AutoApproved --> Closed AutoRejected --> InProgress
State Management
The Issue Tracker Settings uses a hybrid approach combining React Context for local state and Redux for shared state:
IssueTrackerContext
interface IssueTrackerContextState {
// Due Date Settings
isDueDateAutomatic: boolean;
dueDateRules: AutoDueDateRule[];
dueDateLoading: boolean;
// Assignment Settings
isAssignmentAutomatic: boolean;
assignmentHierarchy: AssignmentHierarchy;
// General Settings
generalSettings: GeneralIssueSettings;
// UI State
activeModal: ModalType | null;
selectedRule: AutoDueDateRule | null;
hasUnsavedChanges: boolean;
}
const IssueTrackerProvider: React.FC = ({ children }) => {
const [state, dispatch] = useReducer(
issueTrackerReducer,
initialState
);
// Auto-save functionality
useEffect(() => {
if (state.hasUnsavedChanges) {
const saveTimer = setTimeout(() => {
saveSettings();
}, 5000); // Auto-save after 5 seconds
return () => clearTimeout(saveTimer);
}
}, [state.hasUnsavedChanges]);
return (
<IssueTrackerContext.Provider value={{ state, dispatch }}>
{children}
</IssueTrackerContext.Provider>
);
};API Integration
The Issue Tracker Settings interacts with multiple API endpoints:
1. Due Date Rules API
Get Rules
- Endpoint:
GET /api/v1/issue-tracker/due-date-rules - Query Parameters:
?organizationId={orgId} &page={pageNumber} &limit={pageSize} &search={searchQuery} - Response:
{ "data": [ { "id": "rule123", "name": "Standard SLA", "sites": [{"id": "site1", "name": "Main Office"}], "priorities": { "P0": 1, "P1": 3, "P2": 7 }, "isDefault": false, "isActive": true } ], "total": 25, "page": 1, "limit": 10 }
Create Rule
- Endpoint:
POST /api/v1/issue-tracker/due-date-rules - Headers:
Authorization: Bearer {token} - Payload:
{ "name": "Express SLA", "organizationId": "org123", "sites": ["site1", "site2"], "priorities": { "P0": 0.5, "P1": 1, "P2": 3 }, "isDefault": false }
Update Rule
- Endpoint:
PUT /api/v1/issue-tracker/due-date-rules/{ruleId} - Headers:
Authorization: Bearer {token} - Payload: Same as create
Delete Rule
- Endpoint:
DELETE /api/v1/issue-tracker/due-date-rules/{ruleId} - Headers:
Authorization: Bearer {token}
2. Assignment Settings API
Get Settings
- Endpoint:
GET /api/v1/issue-tracker/assignment-settings/{orgId} - Response:
{ "isAutomatic": true, "hierarchy": ["question", "category", "site", "department"], "enabledLevels": ["question", "category", "site"] }
Update Settings
- Endpoint:
PUT /api/v1/issue-tracker/assignment-settings/{orgId} - Payload:
{ "isAutomatic": true, "hierarchy": ["question", "category", "site", "department"], "enabledLevels": ["question", "category", "site"] }
3. General Settings API
Get Settings
- Endpoint:
GET /api/v1/issue-tracker/general-settings/{orgId} - Response:
{ "auditorCanSubmitForApproval": true, "enableAutoEscalation": false, "defaultApprover": { "type": "hierarchy", "value": "department_head" }, "escalationRules": [] }
Update Settings
- Endpoint:
PUT /api/v1/issue-tracker/general-settings/{orgId} - Payload: Same structure as GET response
4. Approval Settings API
Get Settings
- Endpoint:
GET /api/v1/issue-tracker/approval-settings/{orgId} - Response:
{ "enableAutoApproval": true, "rules": [ { "id": "rule1", "name": "Auto-approve P2 issues", "conditions": [ { "field": "priority", "operator": "equals", "value": "P2" } ], "slaHours": 72, "action": "approve", "includeWeekends": false } ] }
Validation and Business Rules
The Issue Tracker Settings implements comprehensive validation:
1. Due Date Validation
const validateDueDateRule = (rule: DueDateFormData): ValidationResult => {
const errors: ValidationError[] = [];
// Name validation
if (!rule.name || rule.name.trim().length < 3) {
errors.push({
field: 'name',
message: 'Rule name must be at least 3 characters'
});
}
// Priority validation
Object.entries(rule.priorities).forEach(([priority, days]) => {
if (days < 0) {
errors.push({
field: `priorities.${priority}`,
message: `${priority} days cannot be negative`
});
}
if (days > 365) {
errors.push({
field: `priorities.${priority}`,
message: `${priority} days cannot exceed 365`
});
}
});
// Default rule validation
if (rule.isDefault && rule.sites.length > 0) {
errors.push({
field: 'isDefault',
message: 'Default rules cannot be site-specific'
});
}
// Business hours calculation
if (rule.priorities.P0 < 0.5) {
errors.push({
field: 'priorities.P0',
message: 'P0 must be at least 4 business hours (0.5 days)'
});
}
return {
isValid: errors.length === 0,
errors
};
};2. Assignment Hierarchy Validation
const validateAssignmentHierarchy = (
hierarchy: AssignmentLevel[]
): boolean => {
// Ensure all levels are unique
const uniqueLevels = new Set(hierarchy);
if (uniqueLevels.size !== hierarchy.length) {
return false;
}
// Ensure all required levels are present
const requiredLevels = [
AssignmentLevel.REPORTER,
AssignmentLevel.SITE
];
const hasRequiredLevels = requiredLevels.every(level =>
hierarchy.includes(level)
);
return hasRequiredLevels;
};Permission Model
Issue Tracker Settings uses granular permissions:
const issueTrackerPermissions = {
VIEW: 'setting.permission.issue_tracker.view',
EDIT_DUE_DATE: 'setting.permission.issue_tracker.due_date.edit',
EDIT_ASSIGNMENT: 'setting.permission.issue_tracker.assignment.edit',
EDIT_GENERAL: 'setting.permission.issue_tracker.general.edit',
EDIT_APPROVAL: 'setting.permission.issue_tracker.approval.edit',
MANAGE_RULES: 'setting.permission.issue_tracker.rules.manage'
};
const checkPermission = (permission: string): boolean => {
const user = getCurrentUser();
const hasBase = hasPermission(user, issueTrackerPermissions.VIEW);
const hasSpecific = hasPermission(user, permission);
return hasBase && hasSpecific;
};Activity Logging
All changes to Issue Tracker Settings are logged:
const logIssueTrackerActivity = async (
action: string,
details: any
) => {
await createActivityLog({
module: 'ISSUE_TRACKER_SETTINGS',
action,
entityType: 'settings',
entityId: currentOrg.id,
details,
timestamp: new Date().toISOString(),
userId: currentUser.id,
ipAddress: await getClientIP()
});
};
// Example usage
await logIssueTrackerActivity('UPDATE_DUE_DATE_RULE', {
ruleId: rule.id,
changes: {
priorities: {
old: { P0: 1, P1: 3, P2: 7 },
new: { P0: 0.5, P1: 2, P2: 5 }
}
}
});Reports Settings Tab
The Reports Settings tab provides comprehensive configuration options for report generation, photo/video handling, device settings, and audit scheduling. These settings affect how reports are generated, formatted, and distributed throughout the system.
Component Architecture
Main Component: SettingsReport
Location: src/components/settings/SettingsManager/Report/SettingsReport.tsx
Context Provider: SettingsReportContext
Location: src/components/settings/SettingsManager/Report/SettingsReportContext.tsx
The Reports Settings uses React Context for state management:
graph TD subgraph "Reports Settings Architecture" SRC[SettingsReportContext] SRP[SettingsReportProvider] SR[SettingsReport Component] RAP[ReportActivePeriod] end subgraph "State Management" RS[Report Format State] PS[Photo Settings State] DS[Device Settings State] SS[Schedule Settings State] end subgraph "Integration" FB[Firebase Database] RH[Reports Hub] DM[Download Manager] end SRP --> SRC SR --> SRC RAP --> SRC SR --> RAP RS --> SRC PS --> SRC DS --> SRC SS --> SRC SRC --> FB FB --> RH FB --> DM
Configuration Sections
1. Report Format Configuration
Controls the default format for generated reports across the system.
Settings:
-
Report Format
- Options: PDF or XLSX (Excel)
- State:
reportFormat: 'pdf' | 'xlsx' - Default: PDF
- Impact: Affects all report downloads and email attachments
-
Sort by Flag
- Purpose: Determines if report items should be sorted by flag status
- State:
reportSortByFlag: boolean - Impact: Flagged items appear first in reports when enabled
Implementation:
interface ReportFormatSettings {
reportFormat: 'pdf' | 'xlsx';
reportSortByFlag: boolean;
}
const handleReportFormatChange = (format: 'pdf' | 'xlsx') => {
setReportFormat(format);
// Update report generation preferences
updateReportPreferences({
defaultFormat: format,
// PDF-specific settings
pdfOptions: format === 'pdf' ? {
compression: true,
embedFonts: true,
includeMetadata: true
} : null,
// Excel-specific settings
xlsxOptions: format === 'xlsx' ? {
includeFormulas: true,
autoFilter: true,
freezePanes: true
} : null
});
};2. Device Settings
Manages device-specific configurations for offline audits and synchronization.
Settings:
-
Offline Audit Time Source
interface TimeSourceSettings { reportDeviceCheckoutTime: boolean; // true = device time, false = server time }- Device Time: Uses device’s local time for offline audits
- Server Time: Uses server timestamp when syncing
- Impact: Affects audit timestamps and report chronology
-
Cooling Period Configuration
interface CoolingPeriodSettings { isAutoCoolingPeriod: boolean; coolingPeriod: number; // minutes (5-1440) }- Auto Mode: System calculates based on historical data
- Custom Mode: Manual configuration (5-1440 minutes)
- Validation:
const validateCoolingPeriod = (minutes: number): boolean => { if (minutes < 5) { showError('Cooling period must be at least 5 minutes'); return false; } if (minutes > 1440) { showError('Cooling period cannot exceed 24 hours'); return false; } return true; };
Cooling Period Calculation (Auto Mode):
const calculateAutoCoolingPeriod = (
historicalData: AuditHistory[]
): number => {
// Analyze audit patterns
const intervals = historicalData
.sort((a, b) => a.timestamp - b.timestamp)
.map((audit, index, array) => {
if (index === 0) return null;
return audit.timestamp - array[index - 1].timestamp;
})
.filter(Boolean);
// Calculate optimal cooling period
const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
const stdDev = calculateStandardDeviation(intervals);
// Apply business rules
const suggested = Math.round(avgInterval - stdDev);
return Math.max(5, Math.min(suggested, 1440));
};3. Photo/Video Settings
Comprehensive configuration for media capture and quality settings.
Feature Gating:
All photo/video settings require the PHOTO_ATTACHMENT_ANNOTATION feature flag.
Settings:
-
High Resolution Photo/Video
interface ResolutionSettings { highResPhoto: boolean; featureAccessPhoto: boolean; // Feature flag check }- Enabled: Allows capture of high-resolution media
- Disabled: Uses compressed/optimized media
- Storage Impact: High-res media uses 3-5x more storage
-
Photo Source Configuration
interface PhotoSourceSettings { allowGallery: boolean; }- Camera Only: Restricts to live camera capture
- Camera and Gallery: Allows selection from device gallery
- Security Consideration: Gallery access may allow older/edited photos
-
Camera Resolution Settings
interface CameraResolutionSettings { isDefaultCameraResolution: boolean; cameraResolutionSize: 'small' | 'medium' | 'large' | 'original'; }Resolution Options:
- Small: 640x480 (VGA)
- Medium: 1280x720 (HD)
- Large: 1920x1080 (Full HD) - requires high-res enabled
- Original: Device maximum - requires high-res enabled
Resolution Matrix:
const resolutionMatrix = { small: { width: 640, height: 480, quality: 0.7 }, medium: { width: 1280, height: 720, quality: 0.8 }, large: { width: 1920, height: 1080, quality: 0.9 }, original: { width: null, height: null, quality: 1.0 } }; const getAvailableResolutions = (highResEnabled: boolean) => { const base = ['small', 'medium']; return highResEnabled ? [...base, 'large', 'original'] : base; };
Media Processing Pipeline:
const processMediaCapture = async (
media: CapturedMedia,
settings: PhotoVideoSettings
): Promise<ProcessedMedia> => {
// Apply resolution settings
const resolution = settings.isDefaultCameraResolution
? settings.cameraResolutionSize
: await promptUserForResolution();
// Process based on settings
const processed = await mediaProcessor.process({
input: media,
resolution: resolutionMatrix[resolution],
compression: !settings.highResPhoto,
format: 'jpeg',
metadata: {
timestamp: new Date(),
deviceInfo: getDeviceInfo(),
auditContext: getCurrentAuditContext()
}
});
return processed;
};4. Schedule Configuration
Manages default behaviors for audit schedules and user mappings.
Settings:
-
Active Schedule Period
interface ActiveScheduleConfig { useScheduleActivePeriod: boolean; scheduleActivePeriodUnit?: 'day' | 'week' | 'month' | 'year'; scheduleActivePeriodLength?: number; }Component:
ReportActivePeriodLocation: src/components/settings/SettingsManager/Report/ReportActivePeriod.tsxConfiguration Logic:
const calculateScheduleEndDate = ( startDate: Date, config: ActiveScheduleConfig ): Date => { if (!config.useScheduleActivePeriod) { return null; // Indefinite schedule } const { scheduleActivePeriodUnit, scheduleActivePeriodLength } = config; switch (scheduleActivePeriodUnit) { case 'day': return addDays(startDate, scheduleActivePeriodLength); case 'week': return addWeeks(startDate, scheduleActivePeriodLength); case 'month': return addMonths(startDate, scheduleActivePeriodLength); case 'year': return addYears(startDate, scheduleActivePeriodLength); default: return null; } }; -
Auto Mapping Settings
interface ScheduleAutoMapping { singleUserToDept: boolean; multipleUserToDept: boolean; }Auto Mapping Logic:
const performAutoMapping = async ( schedule: Schedule, settings: ScheduleAutoMapping ): Promise<MappingResult> => { const assignments: Assignment[] = []; // Single user to department mapping if (settings.singleUserToDept) { const singleUserDepts = schedule.departments.filter( dept => dept.assignedUsers.length === 1 ); for (const dept of singleUserDepts) { assignments.push({ userId: dept.assignedUsers[0], departmentId: dept.id, type: 'auto_single' }); } } // Multiple users to department mapping if (settings.multipleUserToDept) { const multiUserDepts = schedule.departments.filter( dept => dept.assignedUsers.length > 1 ); for (const dept of multiUserDepts) { // Apply load balancing algorithm const optimalAssignment = calculateOptimalAssignment( dept.assignedUsers, dept.workload ); assignments.push(...optimalAssignment); } } return { assignments, unmappedDepartments: getUnmappedDepartments(schedule) }; };
State Management
The Reports Settings uses a comprehensive React Context for state management:
interface SettingsReportContextState {
// Report Format
reportFormat: 'pdf' | 'xlsx';
setReportFormat: (format: 'pdf' | 'xlsx') => void;
reportSortByFlag: boolean;
setReportSortByFlag: (sort: boolean) => void;
// Device Settings
reportDeviceCheckoutTime: boolean;
setReportDeviceCheckoutTime: (useDevice: boolean) => void;
coolingPeriod: number;
setCoolingPeriod: (minutes: number) => void;
isAutoCoolingPeriod: boolean;
setIsAutoCoolingPeriod: (auto: boolean) => void;
// Photo/Video Settings
highResPhoto: boolean;
setHighResPhoto: (highRes: boolean) => void;
allowGallery: boolean;
setAllowGallery: (allow: boolean) => void;
isDefaultCameraResolution: boolean;
setIsDefaultCameraResolution: (isDefault: boolean) => void;
cameraResolutionSize: CameraResolutionKeys;
setCameraResolutionSize: (size: CameraResolutionKeys) => void;
// Schedule Settings
activeScheduleConfig: ActiveScheduleConfig;
setActiveScheduleConfig: (config: ActiveScheduleConfig) => void;
autoMappingSchedule: ScheduleAutoMapping;
setAutoMappingSchedule: (mapping: ScheduleAutoMapping) => void;
// Feature Access
featureAccessPhoto: boolean;
}API Integration
1. Firebase Integration
Settings are persisted to Firebase Realtime Database:
Save Operation:
const saveReportSettings = async (settings: ReportSettings) => {
const organizationRef = firebase
.database()
.ref(`organization/${organizationId}`);
const updates = {
reportFormat: settings.reportFormat,
reportSortByFlag: settings.reportSortByFlag,
highResPhoto: settings.highResPhoto,
allowGallery: settings.allowGallery,
reportDeviceCheckoutTime: settings.reportDeviceCheckoutTime,
coolingPeriod: settings.coolingPeriod,
isAutoCoolingPeriod: settings.isAutoCoolingPeriod,
isDefaultCameraResolution: settings.isDefaultCameraResolution,
cameraResolutionSize: settings.cameraResolutionSize,
useScheduleActivePeriod: settings.activeScheduleConfig.useScheduleActivePeriod,
scheduleActivePeriodUnit: settings.activeScheduleConfig.scheduleActivePeriodUnit,
scheduleActivePeriodLength: settings.activeScheduleConfig.scheduleActivePeriodLength,
scheduleAutoMapping: settings.autoMappingSchedule
};
await organizationRef.update(updates);
// Log activity
await logActivity({
action: 'UPDATE_REPORT_SETTINGS',
module: 'SETTINGS',
details: { updates }
});
};2. Report Generation APIs
Settings affect multiple report endpoints:
-
Compiled Reports API
- Endpoint:
POST /api/v2/reports/compiled-reports - Uses:
reportFormat,reportSortByFlag - Example:
{ "format": "pdf", "sortByFlag": true, "filters": {...} }
- Endpoint:
-
Missed Reports API
- Endpoint:
POST /api/v2/reports/missed-report - Uses:
reportFormat,coolingPeriod
- Endpoint:
-
Issues Report API
- Endpoint:
POST /api/v2/issues/compiled-issues - Uses:
reportFormat,reportSortByFlag,highResPhoto
- Endpoint:
-
Inventory Report API
- Endpoint:
POST /api/v1/inventory/report - Uses:
reportFormat
- Endpoint:
3. Download Manager Integration
The Download Manager saga uses report settings:
function* handleReportDownload(action: DownloadReportAction) {
const settings = yield select(getReportSettings);
const downloadConfig = {
format: settings.reportFormat,
quality: settings.highResPhoto ? 'high' : 'standard',
compression: settings.reportFormat === 'pdf',
includeMedia: true,
mediaQuality: settings.cameraResolutionSize
};
yield call(downloadManager.download, {
...action.payload,
config: downloadConfig
});
}Validation Rules
1. Cooling Period Validation
const coolingPeriodValidation = {
min: 5,
max: 1440,
validate: (value: number): ValidationResult => {
if (value < coolingPeriodValidation.min) {
return {
valid: false,
error: `Minimum cooling period is ${coolingPeriodValidation.min} minutes`
};
}
if (value > coolingPeriodValidation.max) {
return {
valid: false,
error: `Maximum cooling period is ${coolingPeriodValidation.max} minutes (24 hours)`
};
}
return { valid: true };
}
};2. Schedule Period Validation
const schedulePeriodValidation = {
validateLength: (length: number, unit: string): boolean => {
if (length <= 0) return false;
const maxValues = {
day: 365,
week: 52,
month: 12,
year: 10
};
return length <= maxValues[unit];
}
};3. Camera Resolution Dependencies
const resolutionDependencyRules = {
large: { requires: 'highResPhoto', value: true },
original: { requires: 'highResPhoto', value: true }
};
const validateResolutionSelection = (
resolution: string,
settings: PhotoVideoSettings
): boolean => {
const rule = resolutionDependencyRules[resolution];
if (!rule) return true;
return settings[rule.requires] === rule.value;
};Business Logic and Calculations
1. Report Generation Impact
const applyReportSettings = (
report: BaseReport,
settings: ReportSettings
): ProcessedReport => {
// Apply format
report.format = settings.reportFormat;
// Apply sorting
if (settings.reportSortByFlag) {
report.items.sort((a, b) => {
// Flagged items first
if (a.flagged && !b.flagged) return -1;
if (!a.flagged && b.flagged) return 1;
// Then by original order
return a.order - b.order;
});
}
// Apply media settings
report.mediaQuality = settings.highResPhoto ? 'high' : 'standard';
report.includeGalleryImages = settings.allowGallery;
// Apply device time settings
if (settings.reportDeviceCheckoutTime) {
report.timestamps = report.items.map(item => ({
...item,
timestamp: item.deviceTimestamp || item.serverTimestamp
}));
}
return report;
};2. Feature Access Integration
const getEffectivePhotoSettings = (
userSettings: PhotoVideoSettings,
featureAccess: FeatureAccess
): EffectivePhotoSettings => {
const hasPhotoFeature = featureAccess.features[Features.PHOTO_ATTACHMENT_ANNOTATION];
return {
highResPhoto: hasPhotoFeature && userSettings.highResPhoto,
allowGallery: hasPhotoFeature && userSettings.allowGallery,
cameraResolution: hasPhotoFeature
? userSettings.cameraResolutionSize
: 'small',
isLocked: !hasPhotoFeature
};
};Permission Model
const reportSettingsPermissions = {
VIEW: 'setting.permission.reports.view',
EDIT_FORMAT: 'setting.permission.reports.format.edit',
EDIT_DEVICE: 'setting.permission.reports.device.edit',
EDIT_PHOTO: 'setting.permission.reports.photo.edit',
EDIT_SCHEDULE: 'setting.permission.reports.schedule.edit'
};
const canEditSection = (section: string): boolean => {
const user = getCurrentUser();
const basePermission = hasPermission(user, reportSettingsPermissions.VIEW);
const sectionPermission = reportSettingsPermissions[`EDIT_${section.toUpperCase()}`];
return basePermission && hasPermission(user, sectionPermission);
};UI/UX Considerations
-
Conditional UI Elements
- Camera resolution dropdown only visible when default resolution is selected
- Cooling period input disabled in auto mode
- High-res dependent options show lock icon when feature is disabled
-
Real-time Validation
- Cooling period validates on blur
- Invalid values revert to previous valid state
- Error messages appear inline
-
Feature Information
- Info icons explain feature limitations
- Tooltips provide additional context
- Links to upgrade/feature documentation
Activity Logging
All changes to report settings are logged:
const logReportSettingsChange = async (
changes: Partial<ReportSettings>,
previousValues: Partial<ReportSettings>
) => {
const changedFields = Object.keys(changes).filter(
key => changes[key] !== previousValues[key]
);
await createActivityLog({
module: 'REPORT_SETTINGS',
action: 'UPDATE_SETTINGS',
entityType: 'organization',
entityId: organizationId,
details: {
changedFields,
changes: changedFields.reduce((acc, field) => ({
...acc,
[field]: {
old: previousValues[field],
new: changes[field]
}
}), {})
},
timestamp: new Date().toISOString(),
userId: currentUser.id
});
};LMS Settings Tab
The LMS (Learning Management System) Settings tab provides configuration options for managing content download permissions and quiz answer visibility within the organization’s learning platform. These settings control how different user roles interact with educational content and assessments.
Component Architecture
Main Component: LMSSettings
Location: src/components/settings/SettingsManager/LMS/lmsSettings.tsx
The LMS Settings component follows a simple yet effective architecture:
graph TD subgraph "LMS Settings Architecture" LS[LMSSettings Component] RS[Redux Store] API[LMS Settings API] end subgraph "State Management" LMS_STATE[lmsSettings State] ACTIONS[LMS Actions] REDUCER[LMS Reducer] end subgraph "Integration Points" LFP[LessonFilePreview] CQS[CourseQuizSummary] DM[Download Manager] end LS --> RS RS --> LMS_STATE ACTIONS --> REDUCER REDUCER --> LMS_STATE LS --> API LMS_STATE --> LFP LMS_STATE --> CQS LMS_STATE --> DM
Configuration Options
The LMS Settings tab provides three core configuration options that control content access and quiz behavior:
1. Allow Download Content
Controls whether admin and instructor roles can download course content.
Field: allowDownloadContent
Type: boolean
Default: false
Implementation:
interface ContentDownloadSettings {
allowDownloadContent: boolean;
}
const handleDownloadPermission = (userRole: UserRole): boolean => {
if (userRole === UserRole.LEARNER) {
return false; // Learners use separate setting
}
return userRole === UserRole.ADMIN || userRole === UserRole.INSTRUCTOR
? settings.allowDownloadContent
: false;
};Impact:
- Affects download buttons in course content viewers
- Controls file export functionality in lesson materials
- Applies to PDF, video, and document downloads
2. Allow Learner Download Content
Separate control specifically for learner role download permissions.
Field: allowLearnerDownloadContent
Type: boolean
Default: false
Implementation:
interface LearnerDownloadSettings {
allowLearnerDownloadContent: boolean;
}
// Utility function for checking download permissions
const checkAllowDownload = (
settings: LMSSettingsResponse | null,
userRoles: UserRole[]
): boolean => {
if (!settings) return false;
const isLearner = userRoles.some(role =>
role.code === RoleTypes.LEARNER
);
if (isLearner) {
return settings.allowLearnerDownloadContent || false;
}
const isAdminOrInstructor = userRoles.some(role =>
role.code === RoleTypes.ADMIN ||
role.code === RoleTypes.INSTRUCTOR
);
if (isAdminOrInstructor) {
return settings.allowDownloadContent || false;
}
return false;
};Business Logic:
- Provides granular control over learner access
- Prevents unauthorized content distribution
- Maintains content security while allowing flexibility
3. Show Quiz Correct Answers
Controls whether correct answers are displayed in quiz summaries after completion.
Field: showCorrectQuizAnswer
Type: boolean
Default: true
Implementation:
interface QuizAnswerSettings {
showCorrectQuizAnswer: boolean;
}
const renderQuizSummary = (
quiz: Quiz,
userAnswers: UserAnswer[],
settings: LMSSettingsResponse
): QuizSummaryView => {
const showAnswers = settings.showCorrectQuizAnswer || false;
return {
questions: quiz.questions.map(question => ({
...question,
userAnswer: userAnswers.find(a => a.questionId === question.id),
correctAnswer: showAnswers ? question.correctAnswer : null,
explanation: showAnswers ? question.explanation : null
})),
score: calculateScore(quiz, userAnswers),
showCorrectAnswers: showAnswers
};
};Impact on Quiz Summary:
- When enabled: Shows correct answers with explanations
- When disabled: Only shows user’s answers and score
- Affects both immediate feedback and review modes
State Management
Redux Integration
The LMS Settings integrates with Redux for global state management:
State Structure:
interface LMSSettingsState {
data: LMSSettingsResponse | null;
loading: boolean;
error: string | null;
}
interface LMSSettingsResponse {
id: string;
allowDownloadContent: boolean;
allowLearnerDownloadContent: boolean;
showCorrectQuizAnswer: boolean;
organizationId: string;
createdAt: string;
updatedAt: string;
}Actions:
// Action Types
const LMS_SETTINGS_ACTIONS = {
FETCH_REQUEST: 'LMS_SETTINGS_FETCH_REQUEST',
FETCH_SUCCESS: 'LMS_SETTINGS_FETCH_SUCCESS',
FETCH_FAILURE: 'LMS_SETTINGS_FETCH_FAILURE',
UPDATE_REQUEST: 'LMS_SETTINGS_UPDATE_REQUEST',
UPDATE_SUCCESS: 'LMS_SETTINGS_UPDATE_SUCCESS',
UPDATE_FAILURE: 'LMS_SETTINGS_UPDATE_FAILURE',
UPDATE_LOCAL: 'LMS_SETTINGS_UPDATE_LOCAL'
};
// Action Creators
const fetchLMSSettings = () => ({
type: LMS_SETTINGS_ACTIONS.FETCH_REQUEST
});
const updateLMSSettings = (settings: Partial<LMSSettingsResponse>) => ({
type: LMS_SETTINGS_ACTIONS.UPDATE_REQUEST,
payload: settings
});
const updateLocalLMSSettings = (field: string, value: boolean) => ({
type: LMS_SETTINGS_ACTIONS.UPDATE_LOCAL,
payload: { field, value }
});Reducer:
const lmsSettingsReducer = (
state = initialState,
action: AnyAction
): LMSSettingsState => {
switch (action.type) {
case LMS_SETTINGS_ACTIONS.FETCH_REQUEST:
return { ...state, loading: true, error: null };
case LMS_SETTINGS_ACTIONS.FETCH_SUCCESS:
return {
...state,
data: action.payload,
loading: false,
error: null
};
case LMS_SETTINGS_ACTIONS.UPDATE_LOCAL:
return {
...state,
data: state.data ? {
...state.data,
[action.payload.field]: action.payload.value
} : null
};
case LMS_SETTINGS_ACTIONS.UPDATE_SUCCESS:
return {
...state,
data: action.payload,
loading: false,
error: null
};
default:
return state;
}
};API Integration
1. Fetch LMS Settings
Endpoint: GET /api/v1/lms/lms-settings
Headers:
{
'Authorization': `Bearer ${firebaseToken}`,
'Content-Type': 'application/json'
}Response:
{
"id": "lms-settings-123",
"allowDownloadContent": false,
"allowLearnerDownloadContent": false,
"showCorrectQuizAnswer": true,
"organizationId": "org-456",
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-20T15:30:00Z"
}2. Update LMS Settings
Endpoint: POST /api/v1/lms/lms-settings/upsert
Headers:
{
'Authorization': `Bearer ${firebaseToken}`,
'Content-Type': 'application/json'
}Payload:
{
"allowDownloadContent": true,
"allowLearnerDownloadContent": false,
"showCorrectQuizAnswer": true
}Response: Same as fetch response
Service Implementation:
class LMSSettingsService {
private apiClient: AxiosInstance;
constructor() {
this.apiClient = axios.create({
baseURL: process.env.REACT_APP_API_URL
});
}
async fetchSettings(): Promise<LMSSettingsResponse> {
const token = await firebase.auth().currentUser?.getIdToken();
const response = await this.apiClient.get('/lms/lms-settings', {
headers: {
Authorization: `Bearer ${token}`
}
});
return response.data;
}
async updateSettings(
settings: Partial<LMSSettingsResponse>
): Promise<LMSSettingsResponse> {
const token = await firebase.auth().currentUser?.getIdToken();
const response = await this.apiClient.post(
'/lms/lms-settings/upsert',
settings,
{
headers: {
Authorization: `Bearer ${token}`
}
}
);
return response.data;
}
}Integration Points
1. LessonFilePreview Component
The LessonFilePreview component checks download permissions before rendering download buttons:
const LessonFilePreview: React.FC<LessonFilePreviewProps> = ({
file,
lessonId
}) => {
const lmsSettings = useSelector(getLMSSettingsSelector);
const userRoles = useSelector(getUserRolesSelector);
const canDownload = useMemo(() =>
checkAllowDownload(lmsSettings, userRoles),
[lmsSettings, userRoles]
);
return (
<div className="file-preview">
<FileViewer file={file} />
{canDownload && (
<Button
onClick={() => downloadFile(file)}
icon={<DownloadIcon />}
>
Download
</Button>
)}
</div>
);
};2. CourseQuizSummary Component
The quiz summary component conditionally renders correct answers:
const CourseQuizSummary: React.FC<QuizSummaryProps> = ({
quiz,
userAnswers
}) => {
const lmsSettings = useSelector(getLMSSettingsSelector);
const showCorrectAnswers = lmsSettings?.showCorrectQuizAnswer || false;
return (
<div className="quiz-summary">
<h2>Quiz Results</h2>
<Score value={calculateScore(quiz, userAnswers)} />
{quiz.questions.map(question => (
<QuestionSummary
key={question.id}
question={question}
userAnswer={getUserAnswer(question.id, userAnswers)}
showCorrectAnswer={showCorrectAnswers}
/>
))}
</div>
);
};3. Download Manager Integration
The download manager respects LMS settings when processing download requests:
function* handleLMSContentDownload(action: DownloadContentAction) {
const lmsSettings = yield select(getLMSSettingsSelector);
const userRoles = yield select(getUserRolesSelector);
const canDownload = checkAllowDownload(lmsSettings, userRoles);
if (!canDownload) {
yield put(showError('You do not have permission to download this content'));
return;
}
// Proceed with download
yield call(downloadContent, action.payload);
}Component Implementation
The main LMS Settings component implementation:
const LmsSettings: React.FC = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
const lmsSettings = useSelector(getLMSSettingsSelector);
// Local state for optimistic updates
const [localSettings, setLocalSettings] = useState({
allowDownloadContent: false,
allowLearnerDownloadContent: false,
showCorrectQuizAnswer: true
});
useEffect(() => {
// Fetch settings on mount
dispatch(fetchLMSSettings());
}, [dispatch]);
useEffect(() => {
// Sync local state with Redux state
if (lmsSettings) {
setLocalSettings({
allowDownloadContent: lmsSettings.allowDownloadContent || false,
allowLearnerDownloadContent: lmsSettings.allowLearnerDownloadContent || false,
showCorrectQuizAnswer: lmsSettings.showCorrectQuizAnswer ?? true
});
}
}, [lmsSettings]);
const handleToggle = (field: keyof typeof localSettings) => {
const newValue = !localSettings[field];
// Update local state immediately for responsive UI
setLocalSettings(prev => ({
...prev,
[field]: newValue
}));
// Update Redux store
dispatch(updateLocalLMSSettings(field, newValue));
};
return (
<Grid container spacing={3}>
<Grid item xs={12}>
<SectionTitle>{t('LMS Settings')}</SectionTitle>
</Grid>
<Grid item xs={12}>
<SettingRow>
<SettingLabel>
{t('Allow admin/instructor to download content')}
</SettingLabel>
<RadioGroup
value={localSettings.allowDownloadContent}
onChange={() => handleToggle('allowDownloadContent')}
>
<Radio value={true} label={t('Allow')} />
<Radio value={false} label={t("Don't Allow")} />
</RadioGroup>
</SettingRow>
</Grid>
<Grid item xs={12}>
<SettingRow>
<SettingLabel>
{t('Allow learner to download content')}
</SettingLabel>
<RadioGroup
value={localSettings.allowLearnerDownloadContent}
onChange={() => handleToggle('allowLearnerDownloadContent')}
>
<Radio value={true} label={t('Allow')} />
<Radio value={false} label={t("Don't Allow")} />
</RadioGroup>
</SettingRow>
</Grid>
<Grid item xs={12}>
<SettingRow>
<SettingLabel>
{t('Show correct quiz answers in summary')}
</SettingLabel>
<RadioGroup
value={localSettings.showCorrectQuizAnswer}
onChange={() => handleToggle('showCorrectQuizAnswer')}
>
<Radio value={true} label={t('Show')} />
<Radio value={false} label={t('Hide')} />
</RadioGroup>
</SettingRow>
</Grid>
</Grid>
);
};Save Process
The LMS settings are saved as part of the overall settings save process in the SettingsManager:
const handleSave = async () => {
try {
setIsSaving(true);
// Collect all settings including LMS
const lmsSettingsToSave = {
allowDownloadContent: lmsSettings?.allowDownloadContent || false,
allowLearnerDownloadContent: lmsSettings?.allowLearnerDownloadContent || false,
showCorrectQuizAnswer: lmsSettings?.showCorrectQuizAnswer ?? true
};
// Save LMS settings
await lmsSettingsService.updateSettings(lmsSettingsToSave);
// Log activity
await logActivity({
action: 'UPDATE_LMS_SETTINGS',
module: 'SETTINGS',
details: lmsSettingsToSave
});
showSuccess('LMS settings updated successfully');
} catch (error) {
showError('Failed to save LMS settings');
console.error('LMS settings save error:', error);
} finally {
setIsSaving(false);
}
};Validation and Business Rules
1. Permission Hierarchy
const permissionHierarchy = {
ADMIN: {
canModifySettings: true,
canDownload: () => settings.allowDownloadContent,
canViewAnswers: () => true // Admins always see answers
},
INSTRUCTOR: {
canModifySettings: false,
canDownload: () => settings.allowDownloadContent,
canViewAnswers: () => true // Instructors always see answers
},
LEARNER: {
canModifySettings: false,
canDownload: () => settings.allowLearnerDownloadContent,
canViewAnswers: () => settings.showCorrectQuizAnswer
}
};2. Default Values and Fallbacks
const getEffectiveLMSSettings = (
settings: LMSSettingsResponse | null
): EffectiveLMSSettings => {
return {
allowDownloadContent: settings?.allowDownloadContent || false,
allowLearnerDownloadContent: settings?.allowLearnerDownloadContent || false,
showCorrectQuizAnswer: settings?.showCorrectQuizAnswer ?? true // Default true
};
};Testing
The LMS Settings component includes comprehensive test coverage:
describe('LmsSettings Component', () => {
it('should render all three settings options', () => {
const { getByText } = render(<LmsSettings />);
expect(getByText('Allow admin/instructor to download content')).toBeInTheDocument();
expect(getByText('Allow learner to download content')).toBeInTheDocument();
expect(getByText('Show correct quiz answers in summary')).toBeInTheDocument();
});
it('should handle toggle interactions correctly', async () => {
const { getByLabelText } = render(<LmsSettings />);
const allowDownloadRadio = getByLabelText('Allow');
fireEvent.click(allowDownloadRadio);
await waitFor(() => {
expect(mockDispatch).toHaveBeenCalledWith(
updateLocalLMSSettings('allowDownloadContent', true)
);
});
});
it('should handle null settings gracefully', () => {
mockUseSelector.mockReturnValue(null);
const { container } = render(<LmsSettings />);
expect(container).toBeInTheDocument();
// Should use default values
});
});Security Considerations
-
Content Protection
- Download permissions prevent unauthorized distribution
- Separate controls for different user roles
- Server-side validation of download requests
-
Quiz Integrity
- Hiding answers maintains assessment validity
- Prevents cheating in repeated attempts
- Instructor override capabilities
-
API Security
- Firebase authentication required
- Organization-scoped settings
- Audit logging for all changes
Performance Optimization
// Memoized selectors for efficient re-renders
const getLMSSettingsSelector = createSelector(
[state => state.lmsSettings],
lmsSettings => lmsSettings.data
);
const getCanDownloadSelector = createSelector(
[getLMSSettingsSelector, getUserRolesSelector],
(settings, roles) => checkAllowDownload(settings, roles)
);
// Component-level optimization
const LMSSettingsOptimized = React.memo(LmsSettings, (prevProps, nextProps) => {
// Custom comparison logic if needed
return true; // Props are empty, so always equal
});Activity Logging
All LMS settings changes are logged for audit purposes:
const logLMSSettingsChange = async (
changes: Partial<LMSSettingsResponse>,
previousValues: Partial<LMSSettingsResponse>
) => {
const changedFields = Object.keys(changes).filter(
key => changes[key] !== previousValues[key]
);
await createActivityLog({
module: 'LMS_SETTINGS',
action: 'UPDATE_SETTINGS',
entityType: 'organization',
entityId: organizationId,
details: {
changedFields,
changes: changedFields.reduce((acc, field) => ({
...acc,
[field]: {
old: previousValues[field],
new: changes[field]
}
}), {})
},
timestamp: new Date().toISOString(),
userId: currentUser.id,
metadata: {
userRole: currentUser.role,
ip: await getClientIP()
}
});
};Site-wide Settings Tab
The Site-wide Settings tab provides comprehensive configuration options for organization-wide parameters including currency, geo-location features, site calibration, smart recommendations, supervisor permissions, and custom issue deadlines. These settings affect the entire organization’s operational behavior across all sites and users.
Component Architecture
Main Component: Part of SettingsManager
Location: src/components/settings/SettingsManager/SettingsManager.tsx:1084-1265
The Site-wide Settings integrates with multiple data sources and services:
graph TD subgraph "Site-wide Settings Architecture" SW[Site-wide Settings Section] CURR[Currency Service] GEO[Geo-location Config] CD[Custom Deadlines] end subgraph "Data Sources" FRTDB[Firebase RTDB] FS[Firestore] API[REST APIs] end subgraph "Integration" ORG[Organization State] FA[Feature Access] QC[Question Categories] end SW --> CURR SW --> GEO SW --> CD CURR --> API GEO --> FRTDB CD --> FS CD --> QC SW --> ORG SW --> FA
Configuration Sections
1. Currency Configuration
Allows selection of the organization’s default currency from a comprehensive list of global currencies.
Field: defaultCurrency
Type: string
Default: 'IDR' (Indonesian Rupiah)
Implementation:
interface CurrencySettings {
defaultCurrency: string;
}
interface Currency {
id: string;
code: string;
name: string;
symbol: string;
country: string;
}
const CurrencyConfiguration: React.FC = () => {
const [currencies, setCurrencies] = useState<Currency[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchCurrencies();
}, []);
const fetchCurrencies = async () => {
try {
const response = await currencyService.getCurrencies();
setCurrencies(response.data);
} catch (error) {
console.error('Failed to fetch currencies:', error);
// Use fallback currencies
setCurrencies(DEFAULT_CURRENCIES);
} finally {
setLoading(false);
}
};
return (
<Select
value={defaultCurrency}
onChange={handleCurrencyChange}
loading={loading}
>
{currencies.map(currency => (
<MenuItem key={currency.code} value={currency.code}>
{currency.name} ({currency.symbol})
</MenuItem>
))}
</Select>
);
};Available Currencies (fetched from API):
const SAMPLE_CURRENCIES = [
{ code: 'IDR', name: 'Indonesian Rupiah', symbol: 'Rp' },
{ code: 'USD', name: 'US Dollar', symbol: '$' },
{ code: 'EUR', name: 'Euro', symbol: '€' },
{ code: 'GBP', name: 'British Pound', symbol: '£' },
{ code: 'JPY', name: 'Japanese Yen', symbol: '¥' },
{ code: 'AUD', name: 'Australian Dollar', symbol: 'A$' },
{ code: 'CAD', name: 'Canadian Dollar', symbol: 'C$' },
{ code: 'CHF', name: 'Swiss Franc', symbol: 'Fr' },
{ code: 'CNY', name: 'Chinese Yuan', symbol: '¥' },
{ code: 'SEK', name: 'Swedish Krona', symbol: 'kr' },
// ... and many more
];Impact:
- Affects all financial displays and calculations
- Used in budget tracking, cost reports, and financial analytics
- Currency symbol appears in all monetary fields
2. Geo-location / Geo-fence Configuration
Controls the radius for location-based check-ins and geo-fence boundaries.
Field: geoPrecision
Type: 'small' | 'medium' | 'large' | 'custom' | 'no'
Custom Radius Field: customRadius (when geoPrecision is ‘custom’)
Radius Values:
const GEO_PRECISION_VALUES = {
small: 100, // 100 meters
medium: 250, // 250 meters
large: 500, // 500 meters
custom: null, // User-defined
no: null // Geo-fence disabled
};
interface GeoLocationSettings {
geoPrecision: GeoPrecisionType;
customRadius?: number; // Required when geoPrecision is 'custom'
}Implementation:
const GeoLocationConfiguration: React.FC = () => {
const [geoPrecision, setGeoPrecision] = useState<GeoPrecisionType>('medium');
const [customRadius, setCustomRadius] = useState<number>(100);
const [radiusError, setRadiusError] = useState<string>('');
const handleGeoPrecisionChange = (value: GeoPrecisionType) => {
setGeoPrecision(value);
// Clear custom radius error when switching away from custom
if (value !== 'custom') {
setRadiusError('');
}
};
const validateCustomRadius = (value: number): boolean => {
if (value <= 0) {
setRadiusError('Radius must be greater than 0');
return false;
}
if (value > 10000) {
setRadiusError('Radius cannot exceed 10km');
return false;
}
setRadiusError('');
return true;
};
const getEffectiveRadius = (): number | null => {
if (geoPrecision === 'no') return null;
if (geoPrecision === 'custom') return customRadius;
return GEO_PRECISION_VALUES[geoPrecision];
};
return (
<div>
<RadioGroup value={geoPrecision} onChange={handleGeoPrecisionChange}>
<Radio value="small" label="Small (100m)" />
<Radio value="medium" label="Medium (250m)" />
<Radio value="large" label="Large (500m)" />
<Radio value="custom" label="Custom" />
<Radio value="no" label="Disable geo-fence" />
</RadioGroup>
{geoPrecision === 'custom' && (
<TextField
type="number"
value={customRadius}
onChange={(e) => setCustomRadius(Number(e.target.value))}
onBlur={() => validateCustomRadius(customRadius)}
error={!!radiusError}
helperText={radiusError}
label="Custom radius (meters)"
InputProps={{
endAdornment: <span>meters</span>
}}
/>
)}
</div>
);
};Business Logic:
- Used for check-in validation during audits
- Determines if user is within acceptable range of site
- Affects mobile app location tracking features
- Can be overridden at site level if needed
3. Site Calibration
Feature-flagged setting that enables site calibration functionality.
Field: siteCalibration
Type: boolean
Feature Flag: GEO_TAGGING
Default: false
Implementation:
interface SiteCalibrationSettings {
siteCalibration: boolean;
featureAccessGeoTagging: boolean; // Feature flag
}
const SiteCalibrationConfiguration: React.FC = () => {
const featureAccessGeoTagging = useSelector(
state => state.featureAccess.features[Features.GEO_TAGGING]
);
const isLocked = !featureAccessGeoTagging;
return (
<SettingRow>
<SettingLabel>
Site Calibration
{isLocked && (
<Tooltip title="This feature requires GEO_TAGGING access">
<LockIcon />
</Tooltip>
)}
</SettingLabel>
<RadioGroup
value={siteCalibration}
onChange={handleSiteCalibrationChange}
disabled={isLocked}
>
<Radio value={true} label="Enable" />
<Radio value={false} label="Disable" />
</RadioGroup>
{isLocked && (
<FeatureInfo>
Upgrade to access site calibration features
</FeatureInfo>
)}
</SettingRow>
);
};Functionality:
- Allows precise site location mapping
- Enables boundary adjustment for complex sites
- Improves check-in accuracy for irregular site shapes
- Used in conjunction with geo-fence settings
4. Smart Recommendations
Toggles the AI-powered recommendation system for audits and issues.
Field: smartRecommendations
Type: boolean
Default: true
Implementation:
interface SmartRecommendationSettings {
smartRecommendations: boolean;
}
const handleSmartRecommendationsToggle = (enabled: boolean) => {
setSmartRecommendations(enabled);
// Update recommendation engine configuration
if (enabled) {
initializeRecommendationEngine({
organizationId,
historicalData: true,
mlModels: ['issue_prediction', 'audit_optimization']
});
} else {
disableRecommendationEngine();
}
};Features Affected:
- Suggested corrective actions for issues
- Audit schedule optimization
- Resource allocation recommendations
- Predictive maintenance alerts
- Risk assessment scoring
5. Supervisor Permissions
Configures enhanced permissions for supervisor roles.
Settings:
-
Allow Supervisors to Edit Questionnaires
- Field:
allowSupervisorsToEditQuestionnaires - Type:
boolean - Default:
false
- Field:
-
Allow Supervisors to Edit Deadlines
- Field:
allowSupervisorsToEditDeadlines - Type:
boolean - Default:
false
- Field:
Implementation:
interface SupervisorPermissionSettings {
allowSupervisorsToEditQuestionnaires: boolean;
allowSupervisorsToEditDeadlines: boolean;
}
const SupervisorPermissions: React.FC = () => {
const [permissions, setPermissions] = useState<SupervisorPermissionSettings>({
allowSupervisorsToEditQuestionnaires: false,
allowSupervisorsToEditDeadlines: false
});
const handlePermissionChange = (
permission: keyof SupervisorPermissionSettings,
value: boolean
) => {
setPermissions(prev => ({
...prev,
[permission]: value
}));
// Update role permissions in backend
updateRolePermissions({
role: 'SUPERVISOR',
permissions: {
[permission]: value
}
});
};
return (
<>
<SettingRow>
<SettingLabel>
Allow supervisors to edit questionnaires
</SettingLabel>
<RadioGroup
value={permissions.allowSupervisorsToEditQuestionnaires}
onChange={(value) =>
handlePermissionChange('allowSupervisorsToEditQuestionnaires', value)
}
>
<Radio value={true} label="Yes" />
<Radio value={false} label="No" />
</RadioGroup>
</SettingRow>
<SettingRow>
<SettingLabel>
Allow supervisors to edit deadlines
</SettingLabel>
<RadioGroup
value={permissions.allowSupervisorsToEditDeadlines}
onChange={(value) =>
handlePermissionChange('allowSupervisorsToEditDeadlines', value)
}
>
<Radio value={true} label="Yes" />
<Radio value={false} label="No" />
</RadioGroup>
</SettingRow>
</>
);
};Permission Impact:
const checkSupervisorPermission = (
action: 'EDIT_QUESTIONNAIRE' | 'EDIT_DEADLINE',
userRole: string,
orgSettings: OrganizationSettings
): boolean => {
if (userRole !== 'SUPERVISOR') {
return checkStandardPermission(action, userRole);
}
switch (action) {
case 'EDIT_QUESTIONNAIRE':
return orgSettings.allowSupervisorsToEditQuestionnaires;
case 'EDIT_DEADLINE':
return orgSettings.allowSupervisorsToEditDeadlines;
default:
return false;
}
};6. Custom Issue Deadlines
Dynamic configuration for category-specific issue resolution deadlines.
Structure:
interface CustomDeadline {
id: string;
categoryId: string;
categoryName: string;
deadlineHours: number;
deadlineDays: number; // Calculated from hours
createdAt: Date;
updatedAt: Date;
}
interface CustomDeadlineSettings {
customDeadlines: CustomDeadline[];
availableCategories: QuestionCategory[];
}Implementation:
const CustomDeadlineConfiguration: React.FC = () => {
const [customDeadlines, setCustomDeadlines] = useState<CustomDeadline[]>([]);
const [categories, setCategories] = useState<QuestionCategory[]>([]);
const [newDeadline, setNewDeadline] = useState({
categoryId: '',
days: 1
});
useEffect(() => {
loadCustomDeadlines();
loadCategories();
}, []);
const loadCustomDeadlines = async () => {
try {
const snapshot = await firestore
.collection('customDeadline')
.doc(organizationId)
.get();
if (snapshot.exists) {
const data = snapshot.data();
const deadlines = Object.entries(data.categoriesDeadline || {})
.map(([categoryId, hours]) => ({
id: generateId(),
categoryId,
categoryName: getCategoryName(categoryId),
deadlineHours: hours as number,
deadlineDays: Math.ceil((hours as number) / 24)
}));
setCustomDeadlines(deadlines);
}
} catch (error) {
console.error('Failed to load custom deadlines:', error);
}
};
const addCustomDeadline = () => {
const { categoryId, days } = newDeadline;
// Validation
if (!categoryId) {
showError('Please select a category');
return;
}
if (days <= 0) {
showError('Deadline must be at least 1 day');
return;
}
// Check for duplicates
if (customDeadlines.some(d => d.categoryId === categoryId)) {
showError('Deadline already exists for this category');
return;
}
const category = categories.find(c => c.id === categoryId);
const newDeadlineItem: CustomDeadline = {
id: generateId(),
categoryId,
categoryName: category?.name || '',
deadlineHours: days * 24,
deadlineDays: days,
createdAt: new Date(),
updatedAt: new Date()
};
setCustomDeadlines([...customDeadlines, newDeadlineItem]);
setNewDeadline({ categoryId: '', days: 1 });
};
const removeCustomDeadline = (id: string) => {
setCustomDeadlines(customDeadlines.filter(d => d.id !== id));
};
const saveCustomDeadlines = async () => {
try {
// Convert to API format
const categoriesDeadline = customDeadlines.reduce((acc, deadline) => ({
...acc,
[deadline.categoryId]: deadline.deadlineHours
}), {});
// Save to Firestore
await firestore
.collection('customDeadline')
.doc(organizationId)
.set({
categoriesDeadline,
updatedAt: firebase.firestore.FieldValue.serverTimestamp()
});
// Save to API
await issueService.updateCustomDeadlines({
organizationId,
customDeadlines: categoriesDeadline
});
showSuccess('Custom deadlines saved successfully');
} catch (error) {
showError('Failed to save custom deadlines');
console.error(error);
}
};
return (
<div className="custom-deadlines-section">
<h4>Custom Issue Deadlines</h4>
{/* Add new deadline */}
<div className="add-deadline-row">
<Select
value={newDeadline.categoryId}
onChange={(e) => setNewDeadline({
...newDeadline,
categoryId: e.target.value
})}
placeholder="Select category"
>
{getAvailableCategories().map(category => (
<MenuItem key={category.id} value={category.id}>
{category.name}
</MenuItem>
))}
</Select>
<TextField
type="number"
value={newDeadline.days}
onChange={(e) => setNewDeadline({
...newDeadline,
days: Number(e.target.value)
})}
min={1}
label="Days"
/>
<Button onClick={addCustomDeadline}>
Add Deadline
</Button>
</div>
{/* List existing deadlines */}
<div className="deadlines-list">
{customDeadlines.map(deadline => (
<div key={deadline.id} className="deadline-item">
<span>{deadline.categoryName}</span>
<span>{deadline.deadlineDays} days</span>
<IconButton onClick={() => removeCustomDeadline(deadline.id)}>
<DeleteIcon />
</IconButton>
</div>
))}
</div>
</div>
);
};Available Categories (fetched from API):
const fetchCategories = async (): Promise<QuestionCategory[]> => {
const response = await questionnaireService.getQuestionCategories();
return response.data.filter(category => category.isActive);
};
// Example categories:
const SAMPLE_CATEGORIES = [
{ id: 'cat1', name: 'Safety Violation', code: 'SAFETY' },
{ id: 'cat2', name: 'Quality Issue', code: 'QUALITY' },
{ id: 'cat3', name: 'Maintenance Required', code: 'MAINT' },
{ id: 'cat4', name: 'Compliance', code: 'COMPLY' },
{ id: 'cat5', name: 'Environmental', code: 'ENV' }
];API Integration
1. Currency API
Endpoint: GET /api/v1/miscellaneous/currencies
Response:
{
"data": [
{
"id": "curr_1",
"code": "USD",
"name": "US Dollar",
"symbol": "$",
"country": "United States",
"decimals": 2
},
{
"id": "curr_2",
"code": "EUR",
"name": "Euro",
"symbol": "€",
"country": "European Union",
"decimals": 2
}
]
}2. Question Categories API
Endpoint: GET /api/v1/questionnaires/questioncategories
Response:
{
"data": [
{
"id": "qc_1",
"name": "Safety Violation",
"code": "SAFETY",
"description": "Safety-related issues",
"isActive": true,
"color": "#FF0000",
"icon": "warning"
}
]
}3. Custom Deadlines API
Endpoint: POST /api/v1/issues/issues/custom-deadline
Payload:
{
"organizationId": "org_123",
"customDeadlines": {
"qc_1": 72, // 72 hours (3 days)
"qc_2": 168 // 168 hours (7 days)
}
}4. Firebase Integration
Organization Settings (Firebase RTDB):
const saveOrganizationSettings = async (settings: SiteWideSettings) => {
const updates = {
defaultCurrency: settings.defaultCurrency,
geoPrecision: settings.geoPrecision,
customRadius: settings.customRadius,
siteCalibration: settings.siteCalibration,
smartRecommendations: settings.smartRecommendations,
allowSupervisorsToEditQuestionnaires: settings.allowSupervisorsToEditQuestionnaires,
allowSupervisorsToEditDeadlines: settings.allowSupervisorsToEditDeadlines
};
await firebase
.database()
.ref(`organization/${organizationId}`)
.update(updates);
};Custom Deadlines (Firestore):
const saveCustomDeadlines = async (deadlines: CustomDeadline[]) => {
const categoriesDeadline = deadlines.reduce((acc, d) => ({
...acc,
[d.categoryId]: d.deadlineHours
}), {});
await firestore
.collection('customDeadline')
.doc(organizationId)
.set({
categoriesDeadline,
organizationId,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedBy: currentUser.uid
});
};Validation Rules
1. Geo-location Validation
const validateGeoSettings = (
geoPrecision: GeoPrecisionType,
customRadius?: number
): ValidationResult => {
const errors: string[] = [];
if (geoPrecision === 'custom') {
if (!customRadius || customRadius <= 0) {
errors.push('Custom radius must be greater than 0');
}
if (customRadius && customRadius > 10000) {
errors.push('Custom radius cannot exceed 10,000 meters');
}
if (customRadius && !Number.isInteger(customRadius)) {
errors.push('Custom radius must be a whole number');
}
}
return {
isValid: errors.length === 0,
errors
};
};2. Custom Deadline Validation
const validateCustomDeadline = (
categoryId: string,
days: number,
existingDeadlines: CustomDeadline[]
): ValidationResult => {
const errors: string[] = [];
if (!categoryId) {
errors.push('Category is required');
}
if (days <= 0) {
errors.push('Deadline must be at least 1 day');
}
if (days > 365) {
errors.push('Deadline cannot exceed 365 days');
}
if (existingDeadlines.some(d => d.categoryId === categoryId)) {
errors.push('A deadline already exists for this category');
}
return {
isValid: errors.length === 0,
errors
};
};Business Logic
1. Currency Impact Calculation
const calculateCurrencyImpact = (
oldCurrency: string,
newCurrency: string
): CurrencyImpact => {
return {
affectedModules: [
'Budget Management',
'Cost Reports',
'Financial Analytics',
'Invoice Generation',
'Expense Tracking'
],
conversionRequired: oldCurrency !== newCurrency,
historicalDataAffected: true,
requiresRecalculation: [
'budget_allocations',
'cost_summaries',
'financial_reports'
]
};
};2. Geo-fence Business Rules
const applyGeoFenceRules = (
userLocation: Coordinates,
siteLocation: Coordinates,
settings: GeoLocationSettings
): GeoFenceResult => {
if (settings.geoPrecision === 'no') {
return { allowed: true, reason: 'Geo-fence disabled' };
}
const distance = calculateDistance(userLocation, siteLocation);
const radius = settings.geoPrecision === 'custom'
? settings.customRadius
: GEO_PRECISION_VALUES[settings.geoPrecision];
if (distance <= radius) {
return { allowed: true, distance };
}
return {
allowed: false,
distance,
reason: `Outside geo-fence (${distance}m from site, limit: ${radius}m)`
};
};3. Smart Recommendations Engine
const SmartRecommendationsEngine = {
initialize: (enabled: boolean, orgId: string) => {
if (!enabled) {
return { status: 'disabled' };
}
return {
status: 'active',
models: [
'issue_prediction_model',
'audit_optimization_model',
'resource_allocation_model'
],
features: [
'predictive_maintenance',
'audit_schedule_optimization',
'risk_scoring',
'corrective_action_suggestions'
]
};
},
generateRecommendations: async (context: AuditContext) => {
if (!context.smartRecommendationsEnabled) {
return [];
}
const recommendations = await Promise.all([
generateIssueRecommendations(context),
generateScheduleRecommendations(context),
generateResourceRecommendations(context)
]);
return recommendations.flat();
}
};Permission Model
const siteWideSettingsPermissions = {
VIEW: 'setting.permission.sitewide.view',
EDIT_CURRENCY: 'setting.permission.sitewide.currency.edit',
EDIT_GEO: 'setting.permission.sitewide.geo.edit',
EDIT_CALIBRATION: 'setting.permission.sitewide.calibration.edit',
EDIT_RECOMMENDATIONS: 'setting.permission.sitewide.recommendations.edit',
EDIT_SUPERVISOR_PERMS: 'setting.permission.sitewide.supervisor_perms.edit',
EDIT_DEADLINES: 'setting.permission.sitewide.deadlines.edit'
};
const canEditSiteWideSection = (section: string): boolean => {
const user = getCurrentUser();
const basePermission = hasPermission(user, siteWideSettingsPermissions.VIEW);
const sectionPermission = siteWideSettingsPermissions[`EDIT_${section.toUpperCase()}`];
return basePermission && hasPermission(user, sectionPermission);
};Activity Logging
All site-wide settings changes are logged:
const logSiteWideSettingsChange = async (
section: string,
changes: any,
previousValues: any
) => {
const changeDetails = {
section,
changes: Object.keys(changes).reduce((acc, key) => {
if (changes[key] !== previousValues[key]) {
acc[key] = {
old: previousValues[key],
new: changes[key]
};
}
return acc;
}, {})
};
await createActivityLog({
module: 'SITE_WIDE_SETTINGS',
action: `UPDATE_${section.toUpperCase()}`,
entityType: 'organization',
entityId: organizationId,
details: changeDetails,
timestamp: new Date().toISOString(),
userId: currentUser.id,
impact: calculateSettingsImpact(section, changes)
});
};Save Process
The complete save process for site-wide settings:
const saveSiteWideSettings = async () => {
try {
setIsSaving(true);
// 1. Validate all settings
const validationResults = validateAllSiteWideSettings();
if (!validationResults.isValid) {
showError(validationResults.errors.join(', '));
return;
}
// 2. Prepare updates
const organizationUpdates = {
defaultCurrency,
geoPrecision,
customRadius: geoPrecision === 'custom' ? customRadius : null,
siteCalibration,
smartRecommendations,
allowSupervisorsToEditQuestionnaires,
allowSupervisorsToEditDeadlines
};
// 3. Save to Firebase RTDB
await firebase
.database()
.ref(`organization/${organizationId}`)
.update(organizationUpdates);
// 4. Save custom deadlines to Firestore
if (customDeadlinesModified) {
await saveCustomDeadlines(customDeadlines);
}
// 5. Update backend via API
await organizationService.updateSettings({
organizationId,
settings: organizationUpdates
});
// 6. Log activities
await logSiteWideSettingsChange(
'all',
organizationUpdates,
previousSettings
);
showSuccess('Site-wide settings saved successfully');
setHasUnsavedChanges(false);
} catch (error) {
showError('Failed to save site-wide settings');
console.error('Site-wide settings save error:', error);
} finally {
setIsSaving(false);
}
};Customization Settings Tab
The Customization Settings tab provides brand customization options for organizations, allowing them to upload custom logos for Web Audit and Reports. This tab is exclusively available to admin-level users and enables organizations to maintain their brand identity throughout the platform.
Component Architecture
Main Component: CustomizationOrgs
Location: src/components/settings/SettingsManager/CustomizationOrgs.tsx
The Customization Settings follows a straightforward architecture:
graph TD subgraph "Customization Architecture" CO[CustomizationOrgs Component] FU[File Upload Handler] IP[Image Preview] VS[Validation Service] end subgraph "API Integration" BA[Branding Assets API] BS[Branding Settings API] CS[Cloud Storage] end subgraph "State Management" LS[Local State] RS[Redux Store - Org ID] end CO --> FU CO --> IP FU --> VS FU --> BA BA --> CS CO --> BS CO --> LS CO --> RS
Access Control
The Customization tab is restricted to specific user roles:
const ALLOWED_ROLES = ['account_holder', 'admin', 'superadmin'];
const handleRenderCustomLogosTab = (): boolean => {
const { role } = props.user;
return ALLOWED_ROLES.includes(role.code);
};Logo Configuration Options
The customization settings support two types of logos:
1. Web Audit Logo
Used throughout the web application interface.
Field: webAuditLogo
Options:
- Use default Nimbly logo
- Upload custom logo
2. Report Logo
Displayed on generated reports and exported documents.
Field: reportLogo
Options:
- Use default Nimbly logo
- Upload custom logo
Logo Upload Specifications
File Requirements
interface LogoRequirements {
formats: string[];
maxSizeKB: number;
maxSizeBytes: number;
previewDimensions: {
width: string;
height: string;
};
}
const LOGO_REQUIREMENTS: LogoRequirements = {
formats: ['jpg', 'jpeg', 'png'],
maxSizeKB: 300,
maxSizeBytes: 307200, // 300 * 1024
previewDimensions: {
width: '203.18px',
height: '135px'
}
};Validation Rules
const validateLogoFile = (file: File): ValidationResult => {
const errors: string[] = [];
// File size validation
if (file.size > LOGO_REQUIREMENTS.maxSizeBytes) {
errors.push(`File size must be less than ${LOGO_REQUIREMENTS.maxSizeKB}KB`);
}
// File format validation
const fileExtension = file.name.split('.').pop()?.toLowerCase();
if (!fileExtension || !LOGO_REQUIREMENTS.formats.includes(fileExtension)) {
errors.push(`File format must be: ${LOGO_REQUIREMENTS.formats.join(', ')}`);
}
// Additional validation for corrupt files
if (file.size === 0) {
errors.push('File appears to be empty or corrupt');
}
return {
isValid: errors.length === 0,
errors
};
};Component Implementation
interface CustomizationOrgsProps {
organizationID: string;
user: User;
}
interface LogoState {
webAuditLogo: LogoConfig | null;
reportLogo: LogoConfig | null;
isLoading: boolean;
hasUnsavedChanges: boolean;
}
interface LogoConfig {
useDefault: boolean;
fileId?: string;
downloadUrl?: string;
fileName?: string;
}
const CustomizationOrgs: React.FC<CustomizationOrgsProps> = ({
organizationID,
user
}) => {
const [logoState, setLogoState] = useState<LogoState>({
webAuditLogo: null,
reportLogo: null,
isLoading: true,
hasUnsavedChanges: false
});
const [uploadingLogo, setUploadingLogo] = useState<{
webAudit: boolean;
report: boolean;
}>({
webAudit: false,
report: false
});
useEffect(() => {
fetchBrandingSettings();
}, [organizationID]);
const fetchBrandingSettings = async () => {
try {
const response = await brandingService.getBrandingSettings(organizationID);
if (response.data && response.data.length > 0) {
const branding = response.data[0];
setLogoState({
webAuditLogo: {
useDefault: !branding.webAuditLogo,
fileId: branding.webAuditLogo?.fileId,
downloadUrl: branding.webAuditLogo?.downloadUrl,
fileName: branding.webAuditLogo?.fileName
},
reportLogo: {
useDefault: !branding.reportLogo,
fileId: branding.reportLogo?.fileId,
downloadUrl: branding.reportLogo?.downloadUrl,
fileName: branding.reportLogo?.fileName
},
isLoading: false,
hasUnsavedChanges: false
});
} else {
// No branding settings exist - use defaults
setLogoState({
webAuditLogo: { useDefault: true },
reportLogo: { useDefault: true },
isLoading: false,
hasUnsavedChanges: false
});
}
} catch (error) {
console.error('Failed to fetch branding settings:', error);
showError('Failed to load customization settings');
}
};
const handleLogoTypeChange = (
logoType: 'webAudit' | 'report',
useDefault: boolean
) => {
setLogoState(prev => ({
...prev,
[`${logoType}Logo`]: {
...prev[`${logoType}Logo`],
useDefault
},
hasUnsavedChanges: true
}));
};
const handleFileUpload = async (
logoType: 'webAudit' | 'report',
file: File
) => {
// Validate file
const validation = validateLogoFile(file);
if (!validation.isValid) {
showError(validation.errors.join(', '));
return;
}
setUploadingLogo(prev => ({ ...prev, [logoType]: true }));
try {
// Upload file to cloud storage
const uploadResponse = await brandingService.uploadAsset(
logoType === 'webAudit' ? 'web-audit-logo' : 'report-logo',
file
);
const { fileId, downloadUrl, fileName } = uploadResponse.data;
// Update local state with uploaded file info
setLogoState(prev => ({
...prev,
[`${logoType}Logo`]: {
useDefault: false,
fileId,
downloadUrl,
fileName
},
hasUnsavedChanges: true
}));
showSuccess(`${logoType === 'webAudit' ? 'Web Audit' : 'Report'} logo uploaded successfully`);
} catch (error) {
console.error('Logo upload failed:', error);
showError('Failed to upload logo. Please try again.');
} finally {
setUploadingLogo(prev => ({ ...prev, [logoType]: false }));
}
};
const saveBrandingSettings = async () => {
try {
const brandingData = {
organizationId: organizationID,
webAuditLogo: logoState.webAuditLogo?.useDefault ? null : {
fileId: logoState.webAuditLogo.fileId,
downloadUrl: logoState.webAuditLogo.downloadUrl,
fileName: logoState.webAuditLogo.fileName
},
reportLogo: logoState.reportLogo?.useDefault ? null : {
fileId: logoState.reportLogo.fileId,
downloadUrl: logoState.reportLogo.downloadUrl,
fileName: logoState.reportLogo.fileName
}
};
// Check if branding exists
const existingBranding = await brandingService.getBrandingSettings(organizationID);
if (existingBranding.data && existingBranding.data.length > 0) {
// Update existing branding
await brandingService.updateBrandingSettings(
organizationID,
existingBranding.data[0].id,
brandingData
);
} else {
// Create new branding
await brandingService.createBrandingSettings(
organizationID,
brandingData
);
}
setLogoState(prev => ({ ...prev, hasUnsavedChanges: false }));
showSuccess('Customization settings saved successfully');
// Log activity
await logActivity({
action: 'UPDATE_BRANDING_SETTINGS',
module: 'CUSTOMIZATION',
details: brandingData
});
} catch (error) {
console.error('Failed to save branding settings:', error);
showError('Failed to save customization settings');
}
};
return (
<div className="customization-settings">
<Typography variant="h6" className="section-title">
Custom Logos
</Typography>
<div className="logo-section">
<LogoUploadSection
title="Web Audit Logo"
description="Logo displayed throughout the web application"
logoConfig={logoState.webAuditLogo}
isUploading={uploadingLogo.webAudit}
onTypeChange={(useDefault) => handleLogoTypeChange('webAudit', useDefault)}
onFileUpload={(file) => handleFileUpload('webAudit', file)}
/>
<LogoUploadSection
title="Report Logo"
description="Logo displayed on generated reports"
logoConfig={logoState.reportLogo}
isUploading={uploadingLogo.report}
onTypeChange={(useDefault) => handleLogoTypeChange('report', useDefault)}
onFileUpload={(file) => handleFileUpload('report', file)}
/>
</div>
<div className="action-buttons">
<Button
variant="contained"
color="primary"
onClick={saveBrandingSettings}
disabled={!logoState.hasUnsavedChanges}
>
Save Changes
</Button>
</div>
</div>
);
};Logo Upload Section Component
interface LogoUploadSectionProps {
title: string;
description: string;
logoConfig: LogoConfig | null;
isUploading: boolean;
onTypeChange: (useDefault: boolean) => void;
onFileUpload: (file: File) => void;
}
const LogoUploadSection: React.FC<LogoUploadSectionProps> = ({
title,
description,
logoConfig,
isUploading,
onTypeChange,
onFileUpload
}) => {
const fileInputRef = useRef<HTMLInputElement>(null);
const handleFileSelect = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
onFileUpload(file);
}
// Reset input to allow re-selecting same file
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div className="logo-upload-section">
<Typography variant="subtitle1">{title}</Typography>
<Typography variant="body2" color="textSecondary">
{description}
</Typography>
<RadioGroup
value={logoConfig?.useDefault ? 'default' : 'custom'}
onChange={(e) => onTypeChange(e.target.value === 'default')}
>
<FormControlLabel
value="default"
control={<Radio />}
label="Use default Nimbly logo"
/>
<FormControlLabel
value="custom"
control={<Radio />}
label="Upload custom logo"
/>
</RadioGroup>
{!logoConfig?.useDefault && (
<div className="upload-area">
<input
ref={fileInputRef}
type="file"
accept=".jpg,.jpeg,.png"
onChange={handleFileSelect}
style={{ display: 'none' }}
id={`file-input-${title}`}
/>
<label htmlFor={`file-input-${title}`}>
<Button
variant="outlined"
component="span"
disabled={isUploading}
startIcon={isUploading ? <CircularProgress size={20} /> : <CloudUploadIcon />}
>
{isUploading ? 'Uploading...' : 'Choose File'}
</Button>
</label>
<Tooltip title="JPG, JPEG, PNG - Max 300KB">
<InfoIcon className="info-icon" />
</Tooltip>
{logoConfig?.downloadUrl && (
<div className="preview-container">
<Typography variant="caption">Current Logo:</Typography>
<img
src={logoConfig.downloadUrl}
alt={`${title} preview`}
style={{
width: LOGO_REQUIREMENTS.previewDimensions.width,
height: LOGO_REQUIREMENTS.previewDimensions.height,
objectFit: 'contain',
border: '1px solid #ddd',
borderRadius: '4px',
marginTop: '8px'
}}
/>
<Typography variant="caption" color="textSecondary">
{logoConfig.fileName}
</Typography>
</div>
)}
</div>
)}
</div>
);
};API Integration
1. Upload Branding Asset
Endpoint: POST /api/v1/miscellaneous/branding-assets/{type}
Headers:
{
'Authorization': `Bearer ${token}`,
'Content-Type': 'multipart/form-data'
}Parameters:
type: ‘web-audit-logo’ | ‘report-logo’file: File object (multipart)
Response:
{
"fileId": "file_abc123",
"downloadUrl": "https://storage.example.com/logos/file_abc123.png",
"fileName": "company-logo.png",
"fileSize": 245678,
"mimeType": "image/png"
}2. Get Branding Settings
Endpoint: GET /api/v1/organizations/{organizationId}/branding-settings
Response:
{
"data": [
{
"id": "branding_123",
"organizationId": "org_456",
"webAuditLogo": {
"fileId": "file_abc123",
"downloadUrl": "https://storage.example.com/logos/web-audit.png",
"fileName": "web-audit-logo.png"
},
"reportLogo": {
"fileId": "file_def456",
"downloadUrl": "https://storage.example.com/logos/report.png",
"fileName": "report-logo.png"
},
"createdAt": "2024-01-15T10:00:00Z",
"updatedAt": "2024-01-20T15:30:00Z"
}
]
}3. Create Branding Settings
Endpoint: POST /api/v1/organizations/{organizationId}/branding-settings
Payload:
{
"organizationId": "org_456",
"webAuditLogo": {
"fileId": "file_abc123",
"downloadUrl": "https://storage.example.com/logos/web-audit.png",
"fileName": "web-audit-logo.png"
},
"reportLogo": null
}4. Update Branding Settings
Endpoint: PUT /api/v1/organizations/{organizationId}/branding-settings/{brandingId}
Payload: Same as create
Service Implementation
class BrandingService {
private apiClient: AxiosInstance;
constructor() {
this.apiClient = createApiClient();
}
async uploadAsset(type: 'web-audit-logo' | 'report-logo', file: File) {
const formData = new FormData();
formData.append('file', file);
const response = await this.apiClient.post(
`/miscellaneous/branding-assets/${type}`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
}
}
);
return response;
}
async getBrandingSettings(organizationId: string) {
return this.apiClient.get(
`/organizations/${organizationId}/branding-settings`
);
}
async createBrandingSettings(organizationId: string, data: BrandingData) {
return this.apiClient.post(
`/organizations/${organizationId}/branding-settings`,
data
);
}
async updateBrandingSettings(
organizationId: string,
brandingId: string,
data: BrandingData
) {
return this.apiClient.put(
`/organizations/${organizationId}/branding-settings/${brandingId}`,
data
);
}
}File Upload Process
sequenceDiagram participant User participant Component participant Validation participant API participant Storage participant DB User->>Component: Select file Component->>Validation: Validate file alt File valid Validation-->>Component: OK Component->>API: Upload file API->>Storage: Store file Storage-->>API: File URL & ID API-->>Component: Upload response Component->>Component: Update preview User->>Component: Save settings Component->>API: Save branding API->>DB: Store settings API-->>Component: Success Component-->>User: Show success else File invalid Validation-->>Component: Error Component-->>User: Show error end
Image Processing
The system handles image processing on the server side:
interface ImageProcessingConfig {
maxWidth: number;
maxHeight: number;
quality: number;
format: 'jpeg' | 'png';
preserveAspectRatio: boolean;
}
const WEB_AUDIT_LOGO_CONFIG: ImageProcessingConfig = {
maxWidth: 400,
maxHeight: 200,
quality: 0.9,
format: 'png',
preserveAspectRatio: true
};
const REPORT_LOGO_CONFIG: ImageProcessingConfig = {
maxWidth: 300,
maxHeight: 150,
quality: 0.85,
format: 'png',
preserveAspectRatio: true
};UI/UX Features
1. Real-time Preview
const ImagePreview: React.FC<{ url: string; alt: string }> = ({ url, alt }) => {
const [imageError, setImageError] = useState(false);
return (
<div className="image-preview">
{!imageError ? (
<img
src={url}
alt={alt}
onError={() => setImageError(true)}
style={{
maxWidth: '100%',
maxHeight: '135px',
objectFit: 'contain'
}}
/>
) : (
<div className="image-error">
<BrokenImageIcon />
<Typography variant="caption">
Failed to load image
</Typography>
</div>
)}
</div>
);
};2. Unsaved Changes Warning
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (logoState.hasUnsavedChanges) {
e.preventDefault();
e.returnValue = 'You have unsaved changes. Are you sure you want to leave?';
}
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [logoState.hasUnsavedChanges]);3. Loading States
const LoadingOverlay: React.FC = () => (
<div className="loading-overlay">
<CircularProgress />
<Typography variant="body2">
Uploading logo...
</Typography>
</div>
);Error Handling
Comprehensive error handling for various scenarios:
const handleUploadError = (error: any): string => {
if (error.response?.status === 413) {
return 'File size exceeds maximum limit';
}
if (error.response?.status === 415) {
return 'Unsupported file format';
}
if (error.response?.status === 403) {
return 'You do not have permission to upload logos';
}
if (error.code === 'NETWORK_ERROR') {
return 'Network error. Please check your connection';
}
return 'Failed to upload logo. Please try again';
};Activity Logging
All customization changes are logged:
const logCustomizationActivity = async (
action: 'UPLOAD_LOGO' | 'UPDATE_BRANDING',
details: any
) => {
await createActivityLog({
module: 'CUSTOMIZATION_SETTINGS',
action,
entityType: 'organization',
entityId: organizationID,
details: {
...details,
userAgent: navigator.userAgent,
timestamp: new Date().toISOString()
},
userId: currentUser.id
});
};Best Practices
-
Image Optimization
- Compress images before upload
- Use appropriate formats (PNG for logos with transparency)
- Maintain aspect ratios
-
User Experience
- Provide clear file requirements
- Show upload progress
- Immediate visual feedback
-
Security
- Validate file types on both client and server
- Scan uploaded files for malware
- Store files in secure cloud storage
-
Performance
- Lazy load logo images
- Use CDN for logo delivery
- Cache logos appropriately
Impact on System
The customization settings affect various parts of the system:
-
Web Application
- Header logo
- Login page branding
- Email templates
-
Reports
- PDF report headers
- Excel report branding
- Email report attachments
-
Mobile Applications
- Splash screen logo
- App header branding
Fallback Mechanism
const getLogoUrl = (
brandingSettings: BrandingSettings | null,
logoType: 'webAudit' | 'report'
): string => {
const logoConfig = brandingSettings?.[`${logoType}Logo`];
if (logoConfig && !logoConfig.useDefault && logoConfig.downloadUrl) {
return logoConfig.downloadUrl;
}
// Fallback to default Nimbly logo
return logoType === 'webAudit'
? '/assets/images/nimbly-logo-web.png'
: '/assets/images/nimbly-logo-report.png';
};API Endpoints
This section provides a comprehensive overview of all API endpoints used by the General Settings module across all six tabs.
Settings API Endpoints
| Endpoint | Method | Purpose | Tab | Parameters | Request Body | Response |
|---|---|---|---|---|---|---|
/organization/{orgId} | PUT | Update organization settings | General | orgId | { reportPeriod, timezone, blockedTabVisibility, minimumPasswordLength, requireSpecialCharacters, enforcePasswordReuse } | { success: boolean } |
/organization/{orgId}/password-policy | PUT | Update password policy | General | orgId | { minimumLength, requireSpecialCharacters, enforceReuse, maxAttempts } | { success: boolean } |
/organization/{orgId}/notification-settings | GET | Get notification settings | Issue Tracker | orgId | - | NotificationSettings |
/organization/{orgId}/notification-settings | PUT | Update notification settings | Issue Tracker | orgId | { emailNotifications, pushNotifications, webhookUrl } | { success: boolean } |
/organization/{orgId}/escalation-rules | GET | Get escalation rules | Issue Tracker | orgId | - | EscalationRule[] |
/organization/{orgId}/escalation-rules | POST | Create escalation rule | Issue Tracker | orgId | { priority, timeThreshold, escalateTo, actions } | { ruleId: string } |
/organization/{orgId}/escalation-rules/{ruleId} | PUT | Update escalation rule | Issue Tracker | orgId, ruleId | { priority, timeThreshold, escalateTo, actions } | { success: boolean } |
/organization/{orgId}/escalation-rules/{ruleId} | DELETE | Delete escalation rule | Issue Tracker | orgId, ruleId | - | { success: boolean } |
/api/v2/reports/compiled-reports | POST | Generate compiled reports | Reports | - | { format, sortByFlag, filters, dateRange } | { reportId: string, downloadUrl: string } |
/api/v2/reports/missed-report | POST | Generate missed reports | Reports | - | { format, coolingPeriod, filters } | { reportId: string, downloadUrl: string } |
/api/v2/issues/compiled-issues | POST | Generate issues report | Reports | - | { format, sortByFlag, highResPhoto, filters } | { reportId: string, downloadUrl: string } |
/api/v1/inventory/report | POST | Generate inventory report | Reports | - | { format, filters, includeImages } | { reportId: string, downloadUrl: string } |
/organization/{orgId}/lms-settings | GET | Get LMS settings | LMS | orgId | - | { allowLearnerDownloadContent, showQuizAnswerAfterCompletion } |
/organization/{orgId}/lms-settings | PUT | Update LMS settings | LMS | orgId | { allowLearnerDownloadContent, showQuizAnswerAfterCompletion } | { success: boolean } |
/organization/{orgId}/custom-deadlines | GET | Get custom deadlines | Site-wide | orgId | - | CustomDeadline[] |
/organization/{orgId}/custom-deadlines | POST | Create custom deadline | Site-wide | orgId | { category, deadlineHours, conditions } | { deadlineId: string } |
/organization/{orgId}/custom-deadlines/{deadlineId} | PUT | Update custom deadline | Site-wide | orgId, deadlineId | { category, deadlineHours, conditions } | { success: boolean } |
/organization/{orgId}/custom-deadlines/{deadlineId} | DELETE | Delete custom deadline | Site-wide | orgId, deadlineId | - | { success: boolean } |
/miscellaneous/branding-assets/{type} | POST | Upload logo file | Customization | type (webAudit|report) | FormData with file | { fileID: string, fileName: string, downloadURL: string } |
/organizations/{orgId}/branding-settings | GET | Get branding settings | Customization | orgId | - | BrandingSettings |
/organizations/{orgId}/branding-settings | POST | Create branding settings | Customization | orgId | { webAuditLogo, reportLogo } | { brandingID: string } |
/organizations/{orgId}/branding-settings/{brandingID} | PUT | Update branding settings | Customization | orgId, brandingID | { webAuditLogo, reportLogo } | { success: boolean } |
Firebase Realtime Database Endpoints
The General Settings module heavily integrates with Firebase Realtime Database for real-time data synchronization. The following paths are used:
| Path | Purpose | Tab | Data Structure |
|---|---|---|---|
/organization/{orgId}/reportPeriod | Store report period settings | General | string (daily|weekly|monthly) |
/organization/{orgId}/timezone | Store timezone settings | General | string (timezone identifier) |
/organization/{orgId}/blockedTabVisibility | Store blocked tab visibility | General | boolean |
/organization/{orgId}/passwordPolicy | Store password policy settings | General | { minimumLength, requireSpecial, enforceReuse } |
/organization/{orgId}/issueTracker | Store issue tracker settings | Issue Tracker | IssueTrackerSettings object |
/organization/{orgId}/dueDateSettings | Store due date automation | Issue Tracker | { enabled, defaultDays, escalationRules } |
/organization/{orgId}/assignmentHierarchy | Store assignment hierarchy | Issue Tracker | { enabled, levels, autoAssign } |
/organization/{orgId}/reportFormat | Store report format preference | Reports | string (xlsx|pdf) |
/organization/{orgId}/reportSortByFlag | Store sort by flag setting | Reports | boolean |
/organization/{orgId}/highResPhoto | Store high resolution photo setting | Reports | boolean |
/organization/{orgId}/allowGallery | Store gallery access setting | Reports | boolean |
/organization/{orgId}/coolingPeriod | Store cooling period setting | Reports | number (hours) |
/organization/{orgId}/cameraResolution | Store camera resolution settings | Reports | { isDefault, size } |
/organization/{orgId}/scheduleSettings | Store schedule-related settings | Reports | { useActivePeriod, unit, length, autoMapping } |
/organization/{orgId}/lmsSettings | Store LMS configuration | LMS | { allowDownload, showQuizAnswers } |
/organization/{orgId}/currency | Store currency preference | Site-wide | string (currency code) |
/organization/{orgId}/geoPrecision | Store geo-location settings | Site-wide | { precision, customRadius } |
/organization/{orgId}/siteCalibration | Store site calibration setting | Site-wide | boolean |
/organization/{orgId}/smartRecommendations | Store smart recommendations setting | Site-wide | boolean |
/organization/{orgId}/supervisorPermissions | Store supervisor permissions | Site-wide | { allowAdhoc, permissions } |
Third-Party Integrations
| Service | Purpose | Tab | Configuration |
|---|---|---|---|
| SendGrid | Email notifications for issue escalations | Issue Tracker | API key in environment variables |
| Firebase Cloud Messaging | Push notifications | Issue Tracker | Project configuration |
| Webhooks | Custom integrations for issue events | Issue Tracker | URL endpoint in settings |
| Cloud Storage (AWS S3/GCS) | Logo file storage | Customization | Bucket configuration |
| CDN | Logo delivery optimization | Customization | Distribution settings |
Authentication & Authorization
All API endpoints require authentication and appropriate permissions:
| Endpoint Pattern | Required Roles | Permission Check |
|---|---|---|
/organization/{orgId}/* | account_holder, admin, superadmin | Organization membership |
/miscellaneous/branding-assets/* | account_holder, admin, superadmin | File upload permissions |
| Report generation endpoints | Based on report type | Role-based access to reports |
| LMS settings | account_holder, admin, superadmin | LMS module access |
| Custom deadlines | account_holder, admin, superadmin | Advanced settings access |
Error Response Format
All API endpoints follow a consistent error response format:
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {
"field": "Specific field error if applicable",
"validation": "Validation error details"
}
},
"timestamp": "2024-01-01T00:00:00.000Z",
"requestId": "unique-request-identifier"
}Rate Limiting
API endpoints implement rate limiting to prevent abuse:
| Endpoint Category | Rate Limit | Window |
|---|---|---|
| Settings updates | 100 requests | 1 hour |
| File uploads | 20 requests | 1 hour |
| Report generation | 50 requests | 1 hour |
| Notification endpoints | 200 requests | 1 hour |
Webhook Payload Examples
For integrations that support webhooks, the following payload structures are used:
Issue Escalation Webhook:
{
"event": "issue.escalated",
"timestamp": "2024-01-01T00:00:00.000Z",
"organizationId": "org123",
"data": {
"issueId": "issue456",
"escalationLevel": 2,
"assignedTo": "user789",
"dueDate": "2024-01-02T00:00:00.000Z",
"priority": "high"
}
}Settings Changed Webhook:
{
"event": "settings.updated",
"timestamp": "2024-01-01T00:00:00.000Z",
"organizationId": "org123",
"data": {
"module": "GENERAL_SETTINGS",
"changedFields": ["timezone", "reportPeriod"],
"updatedBy": "user789",
"previousValues": {
"timezone": "UTC",
"reportPeriod": "daily"
},
"newValues": {
"timezone": "America/New_York",
"reportPeriod": "weekly"
}
}
}Packages and Dependencies
This section outlines all the packages and dependencies used by the General Settings module, organized by category and purpose.
Core Framework Dependencies
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
react | 16.13.1 | Core React library | All components and hooks |
react-dom | 16.13.1 | React DOM rendering | Component rendering |
typescript | 3.x | TypeScript language support | Type safety across all components |
@types/react | 16.9.34 | React TypeScript definitions | Type definitions for React components |
@types/node | 10.x | Node.js TypeScript definitions | Build-time type support |
State Management
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
redux | 4.0.5 | State management library | Global state for all settings |
react-redux | 7.2.0 | React-Redux bindings | Connect components to Redux store |
redux-saga | 1.1.3 | Side effect management | Async operations for settings |
typed-redux-saga | 1.5.0 | Typed Redux Saga | Type-safe sagas |
typesafe-actions | 5.1.0 | Type-safe action creators | Settings action creators |
connected-react-router | 6.8.0 | Router integration with Redux | Navigation state management |
Firebase Integration
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
@firebase/app | 0.4.19 | Firebase core SDK | Firebase initialization |
@firebase/database | 0.5.7 | Realtime Database | Settings persistence |
@firebase/firestore | 1.6.1 | Firestore database | Custom deadlines storage |
@firebase/auth | 0.12.1 | Authentication | User authentication |
@firebase/storage | 0.3.14 | Cloud Storage | Logo file uploads |
@firebase/functions | 0.4.20 | Cloud Functions | API calls |
react-redux-firebase | 3.3.1 | Firebase-Redux integration | Firebase state management |
redux-firestore | 0.13.0 | Firestore-Redux integration | Firestore state management |
UI Component Libraries
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
@radix-ui/react-accordion | 1.2.0 | Accordion component | Settings sections |
@radix-ui/react-dropdown-menu | 2.1.1 | Dropdown menus | Settings dropdowns |
@radix-ui/react-popover | 1.1.1 | Popover component | Tooltips and popovers |
@radix-ui/react-scroll-area | 1.1.0 | Scrollable areas | Settings panels |
@radix-ui/react-separator | 1.1.0 | Visual separators | Section dividers |
@radix-ui/themes | 3.1.1 | Theme system | UI theming |
styled-components | 5.1.0 | CSS-in-JS styling | Component styling |
Form Management
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
formik | 2.2.9 | Form library | Form state management |
react-hook-form | 6.x | Form library | Modern form handling |
@hookform/resolvers | 1.3.7 | Form validation resolvers | Form validation |
yup | 0.32.11 | Schema validation | Form validation schemas |
Data Manipulation and Utilities
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
lodash | 4.17.21 | Utility library | Data manipulation |
moment | 2.24.0 | Date manipulation | Timezone handling |
moment-timezone | 0.5.26 | Timezone support | Timezone calculations |
dayjs | 1.11.12 | Date library | Modern date handling |
classnames | 2.3.2 | CSS class utilities | Conditional styling |
clsx | 2.1.1 | Class name utility | Class concatenation |
Internationalization
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
i18next | 17.0.8 | Internationalization | Text translations |
react-i18next | 10.13.1 | React i18n integration | Component translations |
i18next-http-backend | 1.0.21 | Translation loading | Dynamic translation loading |
Notification and Feedback
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
react-toastify | 8.0.2 | Toast notifications | Settings save feedback |
react-tooltip | 4.2.21 | Tooltips | Settings help text |
File Handling
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
papaparse | 5.2.0 | CSV parsing | Data import/export |
xlsx | 0.16.2 | Excel file handling | Report generation |
Internal Dependencies
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
@nimbly-technologies/nimbly-common | 1.95.3 | Shared types and utilities | Common interfaces |
@nimbly-technologies/audit-component | 1.1.8 | Shared UI components | Reusable components |
Development Dependencies
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
@typescript-eslint/eslint-plugin | 4.26.0 | TypeScript linting | Code quality |
@typescript-eslint/parser | 4.26.0 | TypeScript parser | ESLint TypeScript support |
prettier | 2.0.5 | Code formatting | Consistent code style |
eslint | 7.27.0 | Code linting | Code quality checks |
Testing Dependencies
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
@testing-library/react | 9.3.0 | React testing utilities | Component testing |
@testing-library/jest-dom | 4.x | Jest DOM matchers | DOM testing |
@testing-library/react-hooks | 5.1.3 | Hook testing | Custom hook testing |
redux-mock-store | 1.5.5 | Redux testing | Store mocking |
Build and Deployment
| Package | Version | Purpose | Usage in Settings |
|---|---|---|---|
react-scripts | 3.4.1 | React build tools | Build process |
react-app-rewired | 2.2.1 | Customize React build | Custom build configuration |
firebase-tools | 9.13.0 | Firebase deployment | Deployment to Firebase |
Dependency Analysis by Settings Tab
General Settings Tab Dependencies
- Redux:
redux,react-redux,redux-saga - Firebase:
@firebase/database - Forms:
formik,yup - Date/Time:
moment-timezone,dayjs - Notifications:
react-toastify
Issue Tracker Settings Tab Dependencies
- Redux: Full Redux ecosystem
- Firebase:
@firebase/database,@firebase/functions - Forms:
react-hook-form,@hookform/resolvers - UI Components:
@radix-ui/react-accordion,@radix-ui/react-dropdown-menu - Utilities:
lodash
Reports Settings Tab Dependencies
- Redux: State management
- Firebase: Database and storage
- File Handling:
xlsx,papaparse - Date/Time:
moment,dayjs - Context: React Context API
LMS Settings Tab Dependencies
- Redux: State management
- Firebase: Database integration
- Forms: Form validation
- UI: Basic UI components
Site-wide Settings Tab Dependencies
- Redux: State management
- Firebase: Database and Firestore
- Forms: Form handling
- Utilities:
lodash - Geo: Location-based utilities
Customization Settings Tab Dependencies
- Firebase:
@firebase/storage - File Upload: File handling utilities
- Image Processing: Basic image utilities
- Forms: File input handling
- Notifications:
react-toastify
Security Considerations
| Package | Security Notes | Mitigation |
|---|---|---|
lodash | Prototype pollution vulnerabilities | Use specific imports |
moment | Large bundle size, security issues | Migrate to dayjs |
firebase/* | Requires proper security rules | Implement Firebase security rules |
yup | Schema validation bypass | Use latest version |
Performance Optimization
| Package | Performance Notes | Optimization |
|---|---|---|
moment | Large bundle size | Tree-shaking with dayjs |
lodash | Large bundle size | Use specific imports |
@radix-ui/* | Component library | Lazy loading |
styled-components | Runtime styling | CSS optimization |
Bundle Size Impact
| Category | Total Size | Impact |
|---|---|---|
| React Core | ~45KB | Essential |
| Redux Ecosystem | ~25KB | State management |
| Firebase SDK | ~150KB | Backend integration |
| UI Components | ~80KB | User interface |
| Form Libraries | ~30KB | Form handling |
| Utilities | ~60KB | Helper functions |
| Total Estimated | ~390KB | Settings module |
Version Compatibility Matrix
| React Version | Redux Version | Firebase Version | Compatibility |
|---|---|---|---|
| 16.13.1 | 4.0.5 | 0.4.x | ✅ Stable |
| 17.x | 4.0.5 | 0.5.x | ⚠️ Requires updates |
| 18.x | 4.2.x | 9.x | ⚠️ Major migration needed |
Upgrade Recommendations
-
Priority 1 (Security):
- Update
momenttodayjs(remove security vulnerabilities) - Update
yupto latest version - Update
lodashto latest version
- Update
-
Priority 2 (Performance):
- Implement tree-shaking for
lodash - Optimize
@radix-uiimports - Consider migrating to React 18
- Implement tree-shaking for
-
Priority 3 (Features):
- Update Firebase SDK to v9
- Update Redux to latest version
- Implement React Concurrent Features
Routes and Screens
This section documents all routes and screens related to the settings module and their purposes within the application architecture.
Main Settings Routes
| Route | Component | Purpose | Access Control | Feature Flag |
|---|---|---|---|---|
/settings/general | SettingsPage | Main settings interface with all six tabs | SETTING_PERMISSION_GENERAL_ALL | - |
/settings/notification | NotificationSettingPage | Email/push notification configuration | SETTING_PERMISSION_GENERAL_ALL | - |
/settings/role-manager | RoleManagerPage | User role and permission management | Role-based | - |
/settings/permissions | PermissionsPage | Access control and permissions | SETTING_PERMISSION_ACCESS_ALL | - |
/settings/feature-access | FeatureAccessPage | Feature flag management | Role-based | - |
/settings/integration | IntegrationPage | API integration settings | SETTING_PERMISSION_INTEGRATION_ALL | - |
/settings/federated-account | AccountAccessRequestPage | Federated login management | SETTING_PERMISSION_FEDERATED_ALL | - |
Settings Tab Routes (Internal Navigation)
The main settings page (/settings/general) uses tab-based navigation internally:
| Tab | Internal Route | Component | Purpose | Access Level |
|---|---|---|---|---|
| General | #general | Inline component | Timezone, report periods, password policies | Admin+ |
| Issue Tracker | #issue-tracker | IssueTrackerSettingsPage | Issue workflows, escalation, assignments | Admin+ |
| Reports | #reports | SettingsReport | Report formats, photo settings, schedules | Admin+ |
| LMS | #lms | lmsSettings | Learning management configuration | Admin+ |
| Site-wide | #site-wide | Inline component | Currency, geo-location, recommendations | Admin+ |
| Customization | #customization | CustomizationOrgs | Logo uploads and branding | Admin+ |
Route Configuration Details
1. General Settings Route
- Path:
/settings/general - Component:
SettingsPage→SettingsManagerContainer→SettingsManager - Lazy Loading: Yes, using
@loadable/component - Layout: Wrapped in
Layoutcomponent - Validation: Route validation enabled
- Navigation: Tab-based internal navigation
2. Notification Settings Route
- Path:
/settings/notification - Component:
NotificationSettingPage - Purpose: Configure email/push notifications for various events
- Features:
- Email notification settings
- Push notification preferences
- Webhook configurations
- Notification templates
3. Role Manager Route
- Path:
/settings/role-manager - Component:
RoleManagerPage - Purpose: Manage user roles and associated permissions
- Features:
- Role creation and editing
- Permission assignment
- User role reassignment
- Role hierarchy management
4. Permissions Route
- Path:
/settings/permissions - Component:
PermissionsPage - Purpose: Fine-grained access control management
- Features:
- Resource-based permissions
- Module access control
- Permission inheritance
- Audit trail for permission changes
5. Feature Access Route
- Path:
/settings/feature-access - Component:
FeatureAccessPage - Purpose: Enable/disable application features
- Features:
- Feature flag management
- Module visibility control
- Beta feature access
- Feature dependency management
6. Integration Route
- Path:
/settings/integration - Component:
IntegrationPage - Purpose: Configure external API integrations
- Features:
- API key management
- Webhook configurations
- Third-party service connections
- Integration monitoring
7. Federated Account Route
- Path:
/settings/federated-account - Component:
AccountAccessRequestPage - Purpose: Manage federated login and SSO
- Features:
- SSO configuration
- Identity provider setup
- Access request approval
- Federation monitoring
Navigation Structure
The settings routes are organized in the sidebar navigation under the “Settings” section:
settings: [
{
path: '/settings/general',
icon: { active: GeneralIcon_ACTIVE, inactive: GeneralIcon_INACTIVE },
label: 'nav.general',
id: 'menu_general'
},
{
path: '/settings/notification',
icon: { active: NotificationIcon_ACTIVE, inactive: NotificationIcon_INACTIVE },
label: 'nav.notification',
id: 'menu_notification'
},
// ... other settings routes
]Route Protection and Access Control
Permission-Based Access Control
- General Settings: Requires
SETTING_PERMISSION_GENERAL_ALLpermission - Permissions: Requires
SETTING_PERMISSION_ACCESS_ALLpermission - Integration: Requires
SETTING_PERMISSION_INTEGRATION_ALLpermission - Federated Account: Requires
SETTING_PERMISSION_FEDERATED_ALLpermission
Role-Based Access Control
Settings tabs within the general settings page have role-based restrictions:
| Tab | Required Roles |
|---|---|
| General | ['account_holder', 'admin', 'superadmin'] |
| Issue Tracker | ['account_holder', 'admin', 'superadmin'] |
| Reports | ['account_holder', 'admin', 'superadmin'] |
| LMS | ['account_holder', 'admin', 'superadmin'] |
| Site-wide | ['account_holder', 'admin', 'superadmin'] |
| Customization | ['account_holder', 'admin', 'superadmin'] |
Route Loading Strategy
Lazy Loading Implementation
All settings routes use lazy loading for performance optimization:
const SettingsPage = loadable(() => import('../pages/settings'));
const NotificationSettingPage = loadable(() => import('pages/settings/notification'));
const RoleManagerPage = loadable(() => import('../pages/rolemanager'));
const PermissionsPage = loadable(() => import('../pages/permissions'));
const FeatureAccessPage = loadable(() => import('../pages/featureAccess'));
const IntegrationPage = loadable(() => import('../pages/integration'));
const AccountAccessRequestPage = loadable(() => import('pages/FederatedLogin/AccountAccessRequestPage'));Loading States
- Loading spinner during route transition
- Skeleton components for better UX
- Error boundaries for failed loads
- Fallback components for offline scenarios
Route Validation
All settings routes implement validation middleware:
<Route
exact
path="/settings/general"
component={SettingsPage}
withValidation
access={RoleResources.SETTING_PERMISSION_GENERAL_ALL}
/>Validation Checks
- Authentication: User must be logged in
- Authorization: Required permissions check
- Feature Access: Feature flag validation
- Organization Membership: Valid organization association
- Route Existence: Valid route parameters
URL Parameter Handling
Settings Page URL Structure
- Base Route:
/settings/general - Tab Navigation: Uses hash routing internally
- Query Parameters: Support for deep linking to specific sections
- State Preservation: URL reflects current tab state
Example URL Patterns
/settings/general#general
/settings/general#issue-tracker
/settings/general#reports?section=device-settings
/settings/general#lms
/settings/general#site-wide
/settings/general#customization
Route Transitions and Navigation
Navigation Flow
- User clicks settings menu item
- Route validation middleware executes
- Component lazy loads
- Permission checks run
- Component renders with appropriate data
- Tab state initializes
Back Navigation
- Browser back button support
- Breadcrumb navigation
- Unsaved changes protection
- Tab state restoration
Error Handling
Route-Level Error Handling
- 404 pages for invalid routes
- Permission denied redirects
- Feature not available messages
- Network error recovery
Component-Level Error Handling
- Error boundaries for component failures
- Graceful degradation for missing features
- Retry mechanisms for failed API calls
- User-friendly error messages
SEO and Meta Information
Each settings route includes appropriate meta information:
| Route | Page Title | Meta Description |
|---|---|---|
/settings/general | ”General Settings - Nimbly" | "Configure organization-wide settings” |
/settings/notification | ”Notification Settings - Nimbly" | "Manage notification preferences” |
/settings/role-manager | ”Role Manager - Nimbly" | "Manage user roles and permissions” |
/settings/permissions | ”Permissions - Nimbly" | "Configure access control” |
/settings/feature-access | ”Feature Access - Nimbly" | "Manage feature availability” |
/settings/integration | ”API Integration - Nimbly" | "Configure external integrations” |
/settings/federated-account | ”Federated Accounts - Nimbly" | "Manage SSO and federation” |
Analytics and Tracking
Settings routes include analytics tracking:
Page View Tracking
- Route entry/exit tracking
- Tab interaction tracking
- Time spent on each section
- User navigation patterns
Event Tracking
- Settings changes
- Save actions
- Error occurrences
- Feature usage
Mobile Responsiveness
All settings routes are optimized for mobile devices:
Mobile Navigation
- Collapsible sidebar navigation
- Touch-friendly tab interfaces
- Swipe gestures for tab switching
- Mobile-optimized form layouts
Responsive Design
- Mobile-first CSS approach
- Flexible grid layouts
- Adaptive component sizing
- Touch-optimized interactions
Data Flow Diagrams
This section provides detailed Mermaid diagrams illustrating the data flow and architectural patterns within the General Settings module.
1. Settings Save Process Flow
sequenceDiagram participant User participant Component participant Redux participant Saga participant Firebase participant API User->>Component: Modify setting value Component->>Component: Update local state User->>Component: Click Save button Component->>Redux: Dispatch SAVE_SETTINGS action Redux->>Saga: Trigger save saga alt Firebase Real-time Database Saga->>Firebase: Update organization/{orgId}/setting Firebase-->>Saga: Success response else REST API Saga->>API: POST/PUT to API endpoint API-->>Saga: Success response end Saga->>Redux: Dispatch SAVE_SETTINGS_SUCCESS Redux->>Component: Update Redux state Component->>User: Show success notification Component->>Component: Reset form state
2. Authentication and Authorization Flow
flowchart TD A[User Access Settings] --> B{Authenticated?} B -->|No| C[Redirect to Login] B -->|Yes| D{Has Permission?} D -->|No| E[Show Access Denied] D -->|Yes| F{Feature Enabled?} F -->|No| G[Hide Feature/Tab] F -->|Yes| H[Load Settings Component] H --> I{Organization Member?} I -->|No| J[Show Org Error] I -->|Yes| K{Role Check} K -->|Admin+| L[Show All Tabs] K -->|Supervisor| M[Show Limited Tabs] K -->|Auditor| N[Show Read-only] L --> O[Full Settings Access] M --> P[Restricted Settings Access] N --> Q[View-only Settings]
3. Component Interaction Flow
graph TB subgraph "Page Layer" SP[SettingsPage] end subgraph "Container Layer" SMC[SettingsManagerContainer] SRC[SettingsReportProvider] end subgraph "Component Layer" SM[SettingsManager] subgraph "Tab Components" GS[General Settings] ITS[Issue Tracker Settings] RS[Reports Settings] LMS[LMS Settings] SWS[Site-wide Settings] CS[Customization Settings] end end subgraph "State Management" REDUX[(Redux Store)] CONTEXT[(React Context)] end subgraph "Data Layer" FB[(Firebase)] API[(REST API)] end SP --> SMC SMC --> SRC SRC --> SM SM --> GS SM --> ITS SM --> RS SM --> LMS SM --> SWS SM --> CS SMC <--> REDUX SRC <--> CONTEXT ITS <--> CONTEXT RS <--> CONTEXT REDUX <--> FB REDUX <--> API
4. Settings Data Persistence Architecture
flowchart LR subgraph "Frontend" UI[Settings UI] STATE[Local State] REDUX[Redux Store] end subgraph "Middleware" SAGA[Redux Saga] VALID[Validation Layer] CACHE[Cache Layer] end subgraph "Backend Services" FB[Firebase Realtime DB] FS[Firestore] REST[REST API] STORAGE[Cloud Storage] end UI --> STATE STATE --> REDUX REDUX --> SAGA SAGA --> VALID VALID --> CACHE CACHE --> FB CACHE --> FS CACHE --> REST CACHE --> STORAGE FB -.-> CACHE FS -.-> CACHE REST -.-> CACHE STORAGE -.-> CACHE CACHE -.-> SAGA SAGA -.-> REDUX REDUX -.-> UI
5. Settings Module State Management
stateDiagram-v2 [*] --> Loading Loading --> Loaded: Data fetched successfully Loading --> Error: Fetch failed Loaded --> Editing: User modifies settings Editing --> Validating: User submits changes Validating --> Saving: Validation passed Validating --> Error: Validation failed Saving --> Saved: Save successful Saving --> Error: Save failed Saved --> Loaded: Reset form Error --> Loaded: Retry/Reset Error --> Editing: Fix errors state Loading { [*] --> FetchingSettings FetchingSettings --> FetchingPermissions FetchingPermissions --> FetchingFeatures } state Editing { [*] --> FormDirty FormDirty --> FormValidating FormValidating --> FormDirty: Validation errors FormValidating --> ReadyToSave: Valid form } state Error { [*] --> NetworkError [*] --> ValidationError [*] --> PermissionError [*] --> ServerError }
6. API Integration Data Flow
sequenceDiagram participant Settings as Settings Component participant Redux as Redux Store participant Saga as Redux Saga participant Firebase as Firebase participant API as REST API participant Logger as Activity Logger Note over Settings,Logger: Settings Save Operation Settings->>Redux: Dispatch SAVE_SETTINGS Redux->>Saga: Trigger settingsSaga par Validate Settings Saga->>Saga: Validate settings data and Check Permissions Saga->>Saga: Verify user permissions end alt Firebase Settings Saga->>Firebase: Update organization settings Firebase-->>Saga: Real-time update response else API Settings Saga->>API: POST/PUT settings endpoint API-->>Saga: HTTP response else File Upload (Customization) Saga->>API: POST file upload API-->>Saga: File metadata Saga->>API: PUT branding settings API-->>Saga: Branding response end Saga->>Logger: Log activity Logger-->>Saga: Activity logged alt Success Saga->>Redux: SAVE_SETTINGS_SUCCESS Redux->>Settings: State updated Settings->>Settings: Show success toast else Error Saga->>Redux: SAVE_SETTINGS_FAILURE Redux->>Settings: Error state Settings->>Settings: Show error message end
7. Feature Flag and Permission Resolution
flowchart TD A[Settings Tab Request] --> B{Check Feature Flags} B -->|Disabled| C[Hide Tab] B -->|Enabled| D{Check User Role} D -->|Superadmin| E[Full Access] D -->|Admin| F[Admin Access] D -->|Account Holder| G[Account Holder Access] D -->|Other| H[No Access] E --> I[All Tabs Visible] F --> J{Check Resource Permissions} G --> J H --> K[Redirect/Error] J -->|Has Permission| L[Tab Visible] J -->|No Permission| M[Tab Hidden] I --> N[Load Tab Content] L --> N N --> O{Check Org Membership} O -->|Valid Member| P[Show Settings] O -->|Invalid| Q[Show Error] P --> R{Load Settings Data} R -->|Success| S[Render Settings UI] R -->|Error| T[Show Loading Error]
8. Settings Tab Navigation Flow
flowchart LR subgraph "Tab Navigation" T1[General Tab] T2[Issue Tracker Tab] T3[Reports Tab] T4[LMS Tab] T5[Site-wide Tab] T6[Customization Tab] end subgraph "Shared State" TS[Tab State] US[Unsaved Changes] VS[Validation State] end subgraph "Navigation Logic" TC[Tab Controller] VL[Validation Logic] CW[Change Warning] end T1 --> TC T2 --> TC T3 --> TC T4 --> TC T5 --> TC T6 --> TC TC --> TS TC --> VL VL --> VS VL --> US US --> CW CW -->|Allow| TC CW -->|Block| T1
9. Settings Module Bundle Loading
graph TD A[User Navigates to Settings] --> B[Route Resolution] B --> C{Route Exists?} C -->|No| D[404 Page] C -->|Yes| E[Check Lazy Bundle] E --> F{Bundle Loaded?} F -->|No| G[Load Bundle] F -->|Yes| H[Use Cached Bundle] G --> I{Loading Success?} I -->|No| J[Show Error] I -->|Yes| K[Initialize Component] H --> K K --> L[Check Permissions] L --> M{Authorized?} M -->|No| N[Access Denied] M -->|Yes| O[Render Settings] subgraph "Bundle Contents" P[Settings Components] Q[Redux Logic] R[Styles] S[Type Definitions] end G --> P G --> Q G --> R G --> S
10. Error Handling and Recovery Flow
stateDiagram-v2 [*] --> Normal Normal --> NetworkError: Network failure Normal --> ValidationError: Invalid data Normal --> PermissionError: Access denied Normal --> ServerError: API error NetworkError --> Retrying: Auto retry NetworkError --> OfflineMode: No connection Retrying --> Normal: Retry successful Retrying --> NetworkError: Retry failed OfflineMode --> Normal: Connection restored ValidationError --> Normal: User fixes errors PermissionError --> Normal: Permission granted ServerError --> Normal: Server recovered state NetworkError { [*] --> CheckConnection CheckConnection --> ShowRetryButton ShowRetryButton --> WaitingForRetry } state ValidationError { [*] --> HighlightErrors HighlightErrors --> ShowErrorMessage ShowErrorMessage --> WaitingForFix } state PermissionError { [*] --> ShowAccessDenied ShowAccessDenied --> RedirectToAuth }
These diagrams provide comprehensive visualization of the settings module’s architecture, data flows, and behavioral patterns, making it easier to understand the system’s complexity and interactions.