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:

  1. 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
  2. SettingsManagerContainer (src/components/settings/SettingsManager/SettingsManagerContainer.tsx)

    • Redux container component
    • Maps state and dispatch to props
    • Wraps SettingsManager with SettingsReportProvider context
  3. 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:

  1. Period Unit:

    • Week (7 days)
    • Month (30 days)
    • Year (365 days)
  2. 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:

  1. Simple (Level 1):

    • Minimum 8 characters
    • At least one letter
    • At least one number
  2. 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
  3. 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:

  1. Organization Update API

    • Endpoint: PUT /api/v1/organizations/{organizationId}
    • Headers: Authorization: Bearer {token}
    • Payload:
      {
        "timezone": "America/New_York",
        "displayBlockedTab": true,
        "passwordPolicy": 2
      }
  2. Schedule Update API

    • Endpoint: PUT /api/v1/organizations/{organizationId}/schedule
    • Headers: Authorization: Bearer {token}
    • Payload:
      {
        "periodUnit": "month",
        "periodStart": 1
      }
  3. 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"
            }
          }
        }
      }

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:

  1. Manual Due Date Assignment

    • Auditors manually set due dates for each issue
    • No automated rules applied
    • Provides maximum flexibility
  2. 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:

  1. 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();
        };
    };
  2. 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
  3. 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:

  1. Manual Assignment

    • Issues remain unassigned upon creation
    • Managers/admins manually assign to auditors
    • Suitable for small teams or specific workflows
  2. 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:

  1. Auditor Can Submit for Approval

    • Boolean toggle
    • When enabled, auditors can directly submit issues for approval
    • When disabled, only managers can submit for approval
  2. Enable Auto Escalation

    • Boolean toggle
    • Automatically escalates issues based on SLA breaches
    • Configurable escalation rules and timelines
  3. 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:

  1. SLA Configuration

    • Define time limits for approval actions
    • Separate SLAs for different priority levels
    • Business hours vs calendar hours options
  2. 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[];
    }
  3. 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:

  1. Report Format

    • Options: PDF or XLSX (Excel)
    • State: reportFormat: 'pdf' | 'xlsx'
    • Default: PDF
    • Impact: Affects all report downloads and email attachments
  2. 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:

  1. 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
  2. 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:

  1. 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
  2. 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
  3. 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:

  1. Active Schedule Period

    interface ActiveScheduleConfig {
        useScheduleActivePeriod: boolean;
        scheduleActivePeriodUnit?: 'day' | 'week' | 'month' | 'year';
        scheduleActivePeriodLength?: number;
    }

    Component: ReportActivePeriod Location: src/components/settings/SettingsManager/Report/ReportActivePeriod.tsx

    Configuration 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;
        }
    };
  2. 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:

  1. Compiled Reports API

    • Endpoint: POST /api/v2/reports/compiled-reports
    • Uses: reportFormat, reportSortByFlag
    • Example:
      {
        "format": "pdf",
        "sortByFlag": true,
        "filters": {...}
      }
  2. Missed Reports API

    • Endpoint: POST /api/v2/reports/missed-report
    • Uses: reportFormat, coolingPeriod
  3. Issues Report API

    • Endpoint: POST /api/v2/issues/compiled-issues
    • Uses: reportFormat, reportSortByFlag, highResPhoto
  4. Inventory Report API

    • Endpoint: POST /api/v1/inventory/report
    • Uses: reportFormat

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

  1. 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
  2. Real-time Validation

    • Cooling period validates on blur
    • Invalid values revert to previous valid state
    • Error messages appear inline
  3. 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

  1. Content Protection

    • Download permissions prevent unauthorized distribution
    • Separate controls for different user roles
    • Server-side validation of download requests
  2. Quiz Integrity

    • Hiding answers maintains assessment validity
    • Prevents cheating in repeated attempts
    • Instructor override capabilities
  3. 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:

  1. Allow Supervisors to Edit Questionnaires

    • Field: allowSupervisorsToEditQuestionnaires
    • Type: boolean
    • Default: false
  2. Allow Supervisors to Edit Deadlines

    • Field: allowSupervisorsToEditDeadlines
    • Type: boolean
    • Default: false

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:

Used throughout the web application interface.

Field: webAuditLogo Options:

  • Use default Nimbly logo
  • Upload custom 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

  1. Image Optimization

    • Compress images before upload
    • Use appropriate formats (PNG for logos with transparency)
    • Maintain aspect ratios
  2. User Experience

    • Provide clear file requirements
    • Show upload progress
    • Immediate visual feedback
  3. Security

    • Validate file types on both client and server
    • Scan uploaded files for malware
    • Store files in secure cloud storage
  4. 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:

  1. Web Application

    • Header logo
    • Login page branding
    • Email templates
  2. Reports

    • PDF report headers
    • Excel report branding
    • Email report attachments
  3. 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

EndpointMethodPurposeTabParametersRequest BodyResponse
/organization/{orgId}PUTUpdate organization settingsGeneralorgId{ reportPeriod, timezone, blockedTabVisibility, minimumPasswordLength, requireSpecialCharacters, enforcePasswordReuse }{ success: boolean }
/organization/{orgId}/password-policyPUTUpdate password policyGeneralorgId{ minimumLength, requireSpecialCharacters, enforceReuse, maxAttempts }{ success: boolean }
/organization/{orgId}/notification-settingsGETGet notification settingsIssue TrackerorgId-NotificationSettings
/organization/{orgId}/notification-settingsPUTUpdate notification settingsIssue TrackerorgId{ emailNotifications, pushNotifications, webhookUrl }{ success: boolean }
/organization/{orgId}/escalation-rulesGETGet escalation rulesIssue TrackerorgId-EscalationRule[]
/organization/{orgId}/escalation-rulesPOSTCreate escalation ruleIssue TrackerorgId{ priority, timeThreshold, escalateTo, actions }{ ruleId: string }
/organization/{orgId}/escalation-rules/{ruleId}PUTUpdate escalation ruleIssue TrackerorgId, ruleId{ priority, timeThreshold, escalateTo, actions }{ success: boolean }
/organization/{orgId}/escalation-rules/{ruleId}DELETEDelete escalation ruleIssue TrackerorgId, ruleId-{ success: boolean }
/api/v2/reports/compiled-reportsPOSTGenerate compiled reportsReports-{ format, sortByFlag, filters, dateRange }{ reportId: string, downloadUrl: string }
/api/v2/reports/missed-reportPOSTGenerate missed reportsReports-{ format, coolingPeriod, filters }{ reportId: string, downloadUrl: string }
/api/v2/issues/compiled-issuesPOSTGenerate issues reportReports-{ format, sortByFlag, highResPhoto, filters }{ reportId: string, downloadUrl: string }
/api/v1/inventory/reportPOSTGenerate inventory reportReports-{ format, filters, includeImages }{ reportId: string, downloadUrl: string }
/organization/{orgId}/lms-settingsGETGet LMS settingsLMSorgId-{ allowLearnerDownloadContent, showQuizAnswerAfterCompletion }
/organization/{orgId}/lms-settingsPUTUpdate LMS settingsLMSorgId{ allowLearnerDownloadContent, showQuizAnswerAfterCompletion }{ success: boolean }
/organization/{orgId}/custom-deadlinesGETGet custom deadlinesSite-wideorgId-CustomDeadline[]
/organization/{orgId}/custom-deadlinesPOSTCreate custom deadlineSite-wideorgId{ category, deadlineHours, conditions }{ deadlineId: string }
/organization/{orgId}/custom-deadlines/{deadlineId}PUTUpdate custom deadlineSite-wideorgId, deadlineId{ category, deadlineHours, conditions }{ success: boolean }
/organization/{orgId}/custom-deadlines/{deadlineId}DELETEDelete custom deadlineSite-wideorgId, deadlineId-{ success: boolean }
/miscellaneous/branding-assets/{type}POSTUpload logo fileCustomizationtype (webAudit|report)FormData with file{ fileID: string, fileName: string, downloadURL: string }
/organizations/{orgId}/branding-settingsGETGet branding settingsCustomizationorgId-BrandingSettings
/organizations/{orgId}/branding-settingsPOSTCreate branding settingsCustomizationorgId{ webAuditLogo, reportLogo }{ brandingID: string }
/organizations/{orgId}/branding-settings/{brandingID}PUTUpdate branding settingsCustomizationorgId, 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:

PathPurposeTabData Structure
/organization/{orgId}/reportPeriodStore report period settingsGeneralstring (daily|weekly|monthly)
/organization/{orgId}/timezoneStore timezone settingsGeneralstring (timezone identifier)
/organization/{orgId}/blockedTabVisibilityStore blocked tab visibilityGeneralboolean
/organization/{orgId}/passwordPolicyStore password policy settingsGeneral{ minimumLength, requireSpecial, enforceReuse }
/organization/{orgId}/issueTrackerStore issue tracker settingsIssue TrackerIssueTrackerSettings object
/organization/{orgId}/dueDateSettingsStore due date automationIssue Tracker{ enabled, defaultDays, escalationRules }
/organization/{orgId}/assignmentHierarchyStore assignment hierarchyIssue Tracker{ enabled, levels, autoAssign }
/organization/{orgId}/reportFormatStore report format preferenceReportsstring (xlsx|pdf)
/organization/{orgId}/reportSortByFlagStore sort by flag settingReportsboolean
/organization/{orgId}/highResPhotoStore high resolution photo settingReportsboolean
/organization/{orgId}/allowGalleryStore gallery access settingReportsboolean
/organization/{orgId}/coolingPeriodStore cooling period settingReportsnumber (hours)
/organization/{orgId}/cameraResolutionStore camera resolution settingsReports{ isDefault, size }
/organization/{orgId}/scheduleSettingsStore schedule-related settingsReports{ useActivePeriod, unit, length, autoMapping }
/organization/{orgId}/lmsSettingsStore LMS configurationLMS{ allowDownload, showQuizAnswers }
/organization/{orgId}/currencyStore currency preferenceSite-widestring (currency code)
/organization/{orgId}/geoPrecisionStore geo-location settingsSite-wide{ precision, customRadius }
/organization/{orgId}/siteCalibrationStore site calibration settingSite-wideboolean
/organization/{orgId}/smartRecommendationsStore smart recommendations settingSite-wideboolean
/organization/{orgId}/supervisorPermissionsStore supervisor permissionsSite-wide{ allowAdhoc, permissions }

Third-Party Integrations

ServicePurposeTabConfiguration
SendGridEmail notifications for issue escalationsIssue TrackerAPI key in environment variables
Firebase Cloud MessagingPush notificationsIssue TrackerProject configuration
WebhooksCustom integrations for issue eventsIssue TrackerURL endpoint in settings
Cloud Storage (AWS S3/GCS)Logo file storageCustomizationBucket configuration
CDNLogo delivery optimizationCustomizationDistribution settings

Authentication & Authorization

All API endpoints require authentication and appropriate permissions:

Endpoint PatternRequired RolesPermission Check
/organization/{orgId}/*account_holder, admin, superadminOrganization membership
/miscellaneous/branding-assets/*account_holder, admin, superadminFile upload permissions
Report generation endpointsBased on report typeRole-based access to reports
LMS settingsaccount_holder, admin, superadminLMS module access
Custom deadlinesaccount_holder, admin, superadminAdvanced 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 CategoryRate LimitWindow
Settings updates100 requests1 hour
File uploads20 requests1 hour
Report generation50 requests1 hour
Notification endpoints200 requests1 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

PackageVersionPurposeUsage in Settings
react16.13.1Core React libraryAll components and hooks
react-dom16.13.1React DOM renderingComponent rendering
typescript3.xTypeScript language supportType safety across all components
@types/react16.9.34React TypeScript definitionsType definitions for React components
@types/node10.xNode.js TypeScript definitionsBuild-time type support

State Management

PackageVersionPurposeUsage in Settings
redux4.0.5State management libraryGlobal state for all settings
react-redux7.2.0React-Redux bindingsConnect components to Redux store
redux-saga1.1.3Side effect managementAsync operations for settings
typed-redux-saga1.5.0Typed Redux SagaType-safe sagas
typesafe-actions5.1.0Type-safe action creatorsSettings action creators
connected-react-router6.8.0Router integration with ReduxNavigation state management

Firebase Integration

PackageVersionPurposeUsage in Settings
@firebase/app0.4.19Firebase core SDKFirebase initialization
@firebase/database0.5.7Realtime DatabaseSettings persistence
@firebase/firestore1.6.1Firestore databaseCustom deadlines storage
@firebase/auth0.12.1AuthenticationUser authentication
@firebase/storage0.3.14Cloud StorageLogo file uploads
@firebase/functions0.4.20Cloud FunctionsAPI calls
react-redux-firebase3.3.1Firebase-Redux integrationFirebase state management
redux-firestore0.13.0Firestore-Redux integrationFirestore state management

UI Component Libraries

PackageVersionPurposeUsage in Settings
@radix-ui/react-accordion1.2.0Accordion componentSettings sections
@radix-ui/react-dropdown-menu2.1.1Dropdown menusSettings dropdowns
@radix-ui/react-popover1.1.1Popover componentTooltips and popovers
@radix-ui/react-scroll-area1.1.0Scrollable areasSettings panels
@radix-ui/react-separator1.1.0Visual separatorsSection dividers
@radix-ui/themes3.1.1Theme systemUI theming
styled-components5.1.0CSS-in-JS stylingComponent styling

Form Management

PackageVersionPurposeUsage in Settings
formik2.2.9Form libraryForm state management
react-hook-form6.xForm libraryModern form handling
@hookform/resolvers1.3.7Form validation resolversForm validation
yup0.32.11Schema validationForm validation schemas

Data Manipulation and Utilities

PackageVersionPurposeUsage in Settings
lodash4.17.21Utility libraryData manipulation
moment2.24.0Date manipulationTimezone handling
moment-timezone0.5.26Timezone supportTimezone calculations
dayjs1.11.12Date libraryModern date handling
classnames2.3.2CSS class utilitiesConditional styling
clsx2.1.1Class name utilityClass concatenation

Internationalization

PackageVersionPurposeUsage in Settings
i18next17.0.8InternationalizationText translations
react-i18next10.13.1React i18n integrationComponent translations
i18next-http-backend1.0.21Translation loadingDynamic translation loading

Notification and Feedback

PackageVersionPurposeUsage in Settings
react-toastify8.0.2Toast notificationsSettings save feedback
react-tooltip4.2.21TooltipsSettings help text

File Handling

PackageVersionPurposeUsage in Settings
papaparse5.2.0CSV parsingData import/export
xlsx0.16.2Excel file handlingReport generation

Internal Dependencies

PackageVersionPurposeUsage in Settings
@nimbly-technologies/nimbly-common1.95.3Shared types and utilitiesCommon interfaces
@nimbly-technologies/audit-component1.1.8Shared UI componentsReusable components

Development Dependencies

PackageVersionPurposeUsage in Settings
@typescript-eslint/eslint-plugin4.26.0TypeScript lintingCode quality
@typescript-eslint/parser4.26.0TypeScript parserESLint TypeScript support
prettier2.0.5Code formattingConsistent code style
eslint7.27.0Code lintingCode quality checks

Testing Dependencies

PackageVersionPurposeUsage in Settings
@testing-library/react9.3.0React testing utilitiesComponent testing
@testing-library/jest-dom4.xJest DOM matchersDOM testing
@testing-library/react-hooks5.1.3Hook testingCustom hook testing
redux-mock-store1.5.5Redux testingStore mocking

Build and Deployment

PackageVersionPurposeUsage in Settings
react-scripts3.4.1React build toolsBuild process
react-app-rewired2.2.1Customize React buildCustom build configuration
firebase-tools9.13.0Firebase deploymentDeployment 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

PackageSecurity NotesMitigation
lodashPrototype pollution vulnerabilitiesUse specific imports
momentLarge bundle size, security issuesMigrate to dayjs
firebase/*Requires proper security rulesImplement Firebase security rules
yupSchema validation bypassUse latest version

Performance Optimization

PackagePerformance NotesOptimization
momentLarge bundle sizeTree-shaking with dayjs
lodashLarge bundle sizeUse specific imports
@radix-ui/*Component libraryLazy loading
styled-componentsRuntime stylingCSS optimization

Bundle Size Impact

CategoryTotal SizeImpact
React Core~45KBEssential
Redux Ecosystem~25KBState management
Firebase SDK~150KBBackend integration
UI Components~80KBUser interface
Form Libraries~30KBForm handling
Utilities~60KBHelper functions
Total Estimated~390KBSettings module

Version Compatibility Matrix

React VersionRedux VersionFirebase VersionCompatibility
16.13.14.0.50.4.x✅ Stable
17.x4.0.50.5.x⚠️ Requires updates
18.x4.2.x9.x⚠️ Major migration needed

Upgrade Recommendations

  1. Priority 1 (Security):

    • Update moment to dayjs (remove security vulnerabilities)
    • Update yup to latest version
    • Update lodash to latest version
  2. Priority 2 (Performance):

    • Implement tree-shaking for lodash
    • Optimize @radix-ui imports
    • Consider migrating to React 18
  3. 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

RouteComponentPurposeAccess ControlFeature Flag
/settings/generalSettingsPageMain settings interface with all six tabsSETTING_PERMISSION_GENERAL_ALL-
/settings/notificationNotificationSettingPageEmail/push notification configurationSETTING_PERMISSION_GENERAL_ALL-
/settings/role-managerRoleManagerPageUser role and permission managementRole-based-
/settings/permissionsPermissionsPageAccess control and permissionsSETTING_PERMISSION_ACCESS_ALL-
/settings/feature-accessFeatureAccessPageFeature flag managementRole-based-
/settings/integrationIntegrationPageAPI integration settingsSETTING_PERMISSION_INTEGRATION_ALL-
/settings/federated-accountAccountAccessRequestPageFederated login managementSETTING_PERMISSION_FEDERATED_ALL-

Settings Tab Routes (Internal Navigation)

The main settings page (/settings/general) uses tab-based navigation internally:

TabInternal RouteComponentPurposeAccess Level
General#generalInline componentTimezone, report periods, password policiesAdmin+
Issue Tracker#issue-trackerIssueTrackerSettingsPageIssue workflows, escalation, assignmentsAdmin+
Reports#reportsSettingsReportReport formats, photo settings, schedulesAdmin+
LMS#lmslmsSettingsLearning management configurationAdmin+
Site-wide#site-wideInline componentCurrency, geo-location, recommendationsAdmin+
Customization#customizationCustomizationOrgsLogo uploads and brandingAdmin+

Route Configuration Details

1. General Settings Route

  • Path: /settings/general
  • Component: SettingsPageSettingsManagerContainerSettingsManager
  • Lazy Loading: Yes, using @loadable/component
  • Layout: Wrapped in Layout component
  • 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

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_ALL permission
  • Permissions: Requires SETTING_PERMISSION_ACCESS_ALL permission
  • Integration: Requires SETTING_PERMISSION_INTEGRATION_ALL permission
  • Federated Account: Requires SETTING_PERMISSION_FEDERATED_ALL permission

Role-Based Access Control

Settings tabs within the general settings page have role-based restrictions:

TabRequired 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

  1. Authentication: User must be logged in
  2. Authorization: Required permissions check
  3. Feature Access: Feature flag validation
  4. Organization Membership: Valid organization association
  5. 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

  1. User clicks settings menu item
  2. Route validation middleware executes
  3. Component lazy loads
  4. Permission checks run
  5. Component renders with appropriate data
  6. 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:

RoutePage TitleMeta 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.