1. Overview

The Site Management module provides comprehensive functionality for creating, editing, and managing sites within the Nimbly Audit Admin system. It handles location-based configurations, user assignments, department associations, and scheduling capabilities.

1.1 Key Features

  • Site Creation: Multi-step form for creating new sites with validation
  • Site Editing: Comprehensive edit interface with change tracking
  • Location Management: Support for both address-based and coordinate-based locations
  • User Management: Auditor and supervisor assignment with department-based filtering
  • Scheduling: Quick [schedule](../Schedule/Schedule Listing/ScheduleListingOverview.md) and bulk [schedule](../Schedule/Schedule Listing/ScheduleListingOverview.md) capabilities
  • Activity History: Complete audit trail of all site modifications

2. Architecture

graph TB
    subgraph "Frontend Layer"
        R[Routes] --> P[Pages]
        P --> C[Components]
        C --> S[State/Redux]
    end
    
    subgraph "Components"
        SC[SitesContainer] --> SD[SiteDetails]
        SC --> SM[SitesManager]
        SD --> SDF[SiteDetailsForm]
        SD --> SDA[SiteDetailsAuditors]
        SD --> SDS[SiteDetailsSupervisors]
    end
    
    subgraph "State Layer"
        S --> AS[Admin State]
        S --> SS[Sites State]
        S --> DS[Departments State]
    end
    
    subgraph "API Layer"
        S --> API[API Calls]
        API --> BE[Backend Services]
    end

3. Routes and Screens

Route PathComponentPurposeAccess Control
/admin/sitesSitesManagerSite listing and managementAdmin role required
/admin/sites/newSiteDetailsContainerCreate new siteCreate permission
/admin/sites/:siteIdSiteDetailsContainerEdit existing siteEdit permission
/admin/sites/bulkSitesBulkModalBulk operationsAdmin role
/admin/sites/[schedule](../Schedule/Schedule Listing/ScheduleListingOverview.md)SitesScheduleModalQuick schedulingSchedule permission

API Endpoints

Core Site APIs

EndpointMethodPurposeFile Reference
/sitesGETFetch all sitessrc/services/site.api.ts
/sites/paginateGETFetch paginated sitessrc/services/site.api.ts
/sites/{siteID}/populatedGETFetch single site with relationssrc/services/site.api.ts
/sites/compactGETFetch compact site listsrc/services/sites/siteList.service.ts
/sitesPOSTCreate new sitesrc/services/site.api.ts
/sites/{siteID}PUTUpdate existing sitesrc/services/site.api.ts
/sites/internalPOSTInternal site queriessrc/services/site.api.ts

Site Groups APIs

EndpointMethodPurposeFile Reference
/sites/groupGETFetch site groupssrc/services/sites/siteGroups.services.ts
/sites/groupPOSTCreate site groupsrc/services/sites/siteGroups.services.ts
/sites/groupPUTUpdate site groupsrc/services/sites/siteGroups.services.ts
/sites/group/{groupID}DELETEDelete site groupsrc/services/sites/siteGroups.services.ts

Supporting APIs

EndpointMethodPurposeFile Reference
/[departments](../Departments/DepartmentOverview.md)GETFetch departmentssrc/services/department.api.ts
/users/by-[department](../Departments/DepartmentOverview.md)GETFetch users by departmentsrc/services/user.api.ts
/admin/activity-historyGETFetch activity historysrc/services/admin.api.ts

Core Components

SiteDetailsContainer

Location: src/components/sites/SiteDetails/SiteDetailsContainer.tsx

Primary component for site creation and editing with the following features:

State Management

  • Local state for form validation, location type, timezone data
  • Redux integration for site, department, and user data
  • Change tracking to prevent data loss

Location Handling

  • Address Mode: Google Places autocomplete with Geosuggest
  • Coordinates Mode: Manual latitude/longitude input
  • Automatic timezone detection based on coordinates
  • Google Maps reverse geocoding for address components

Department Management

  • Dynamic department selection and filtering
  • User assignment based on department membership
  • Supervisor and auditor role management

Form Validation

const validation = [
  { field: 'site Name', isValid: true },
  { field: 'auditors', isValid: true },
  { field: 'owner', isValid: true },
  { field: 'address', isValid: true },
  { field: 'timezone', isValid: true },
  { field: 'country', isValid: true },
  { field: 'city', isValid: true },
  { field: 'province', isValid: true }
];

Business Logic

  • Radius Calculation: Supports default, small, medium, large, and custom radius values
  • Coordinate Validation: Ensures valid latitude (-90 to 90) and longitude (-180 to 180)
  • Timezone Mapping: Converts UTC offsets to timezone strings
  • Department Filtering: Filters available users based on selected departments

Site Creation Flow

flowchart TD
    A[User clicks 'New Site'] --> B[SiteDetailsContainer loads]
    B --> C{Form Initialization}
    C --> D[Load Departments]
    C --> E[Load Timezone List]
    C --> F[Initialize Empty Site Object]
    
    D --> G[User fills Site Name]
    G --> H[User selects Location Type]
    
    H --> I{Address or Coordinates?}
    I -->|Address| J[Geosuggest Component]
    I -->|Coordinates| K[Manual Lat/Lng Input]
    
    J --> L[Google Places API]
    L --> M[Auto-populate Address Fields]
    M --> N[Auto-detect Timezone]
    
    K --> O[Reverse Geocoding]
    O --> M
    
    N --> P[Select Departments]
    P --> Q[Assign Auditors/Supervisors]
    Q --> R[Set Radius]
    R --> S[Validation Check]
    
    S -->|Valid| T[Submit to API]
    S -->|Invalid| U[Show Validation Errors]
    U --> G
    
    T --> V[Redux Action]
    V --> W[Saga Process]
    W --> X[API Call]
    X --> Y[Success/Error Handling]
    Y --> Z[Navigate to Site List]

Site Edit Flow

flowchart TD
    A[User clicks Edit Site] --> B[Load Site Data]
    B --> C[Populate Form Fields]
    C --> D[Load Related Data]
    D --> E[Departments, Users, History]
    
    E --> F[User Modifies Fields]
    F --> G[Change Detection]
    G --> H[Enable Save Button]
    G --> I[Show Unsaved Changes Warning]
    
    H --> J[User clicks Save]
    J --> K[Validate Changes]
    K -->|Valid| L[Submit Update]
    K -->|Invalid| M[Show Errors]
    M --> F
    
    L --> N[Update API Call]
    N --> O[Success/Error Handling]
    O --> P[Update Redux State]
    P --> Q[Show Success Message]

Package Dependencies

Maps & Geolocation

PackageVersionPurposeUsage in Site Module
@react-google-maps/apiv1.9.2Google Maps integrationMap display for site location
react-geosuggestv2.12.0Address autocompleteLocation search and selection
google-map-reactv2.1.10Alternative Maps componentBackup map implementation

Form Handling & Validation

PackageVersionPurposeUsage in Site Module
formikv2.2.9Form state managementComplex form handling
yupv0.32.11Schema validationField validation rules
react-hook-formv6Alternative form libraryLightweight form handling
@hookform/resolversv1.3.7Validation resolversIntegration with validation libraries

UI Components

PackageVersionPurposeUsage in Site Module
react-selectv3.1.0Customizable dropdownsDepartment, user, timezone selection
@radix-ui/react-*VariousModern UI primitivesDropdowns, popovers, scrollable areas
react-beautiful-dndv13.1.1Drag and dropSite ordering in lists
styled-componentsv5.1.0CSS-in-JS stylingComponent styling

Data & State Management

PackageVersionPurposeUsage in Site Module
reduxv4.0.5State managementGlobal site state
react-reduxv7.2.0React bindingsComponent state connection
redux-sagav1.1.3Side effectsAPI call management
@tanstack/react-queryv4Server stateCaching and synchronization

Date & Time

PackageVersionPurposeUsage in Site Module
momentv2.24.0Date manipulationSchedule handling
moment-timezonev0.5.26Timezone supportSite timezone management
react-datesv21.8.0Date pickersSchedule date selection

File Processing

PackageVersionPurposeUsage in Site Module
xlsxv0.16.2Excel processingBulk site import/export
papaparsev5.2.0CSV processingData import/export

Utilities

PackageVersionPurposeUsage in Site Module
lodashv4.17.21Utility functionsData manipulation
libphonenumber-jsv1.7.21Phone validationContact information
query-stringv7.0.1URL parametersRoute state management

Nimbly Ecosystem

PackageVersionPurposeUsage in Site Module
@nimbly-technologies/nimbly-commonv1.95.3Shared typesSite, user, department types
@nimbly-technologies/audit-componentv1.1.8Internal componentsReusable UI components

Business Logic and Flows

Location Processing Logic

Google Places Integration

The system integrates with Google Places API for location autocomplete and geocoding:

// Address to coordinates conversion
const handlePlacesChanged = async (place: any) => {
  const lat = parseFloat(place.location.lat);
  const lng = parseFloat(place.location.lng);
  
  // Coordinate validation
  if (isNaN(lat) || isNaN(lng) || lat > 90 || lat < -90 || lng > 180 || lng < -180) {
    return; // Invalid coordinates
  }
  
  // Auto-populate location fields
  setCoordinate({ latitude: lat, longitude: lng });
  setTimezone(lat, lng); // Auto-detect timezone
  setSiteAddress(place.gmaps.formatted_address);
  
  // Extract location components
  place.gmaps.address_components.forEach((component) => {
    if (component.types.indexOf('country') !== -1) {
      setSiteCountry(component.long_name);
    }
    if (component.types.indexOf('administrative_area_level_1') !== -1) {
      setSiteProvince(component.long_name);
    }
    if (component.types.indexOf('administrative_area_level_2') !== -1) {
      setSiteCity(component.long_name);
    }
  });
};

Timezone Detection

Automatic timezone detection based on coordinates:

const timezoneDetection = async (lat: number, lng: number) => {
  const res = await getTimezone(lat, lng);
  if (res.status === 'OK') {
    setSiteTimezone(res.rawOffset / 60, res.timeZoneId);
  }
};

Radius Calculation Logic

Sites support different radius configurations for geofencing:

const radiusOptions = [
  { label: 'Default', value: 'default' },
  { label: 'Small (50m)', value: 'small' },
  { label: 'Medium (100m)', value: 'medium' },
  { label: 'Large (200m)', value: 'large' },
  { label: 'Custom', value: 'custom' }
];
 
// Custom radius validation
const validateCustomRadius = (value: number) => {
  return value > 0 && value <= 1000; // Max 1km radius
};

Department Assignment Logic

Users are filtered and assigned based on department membership:

// Department-based user filtering
const getDepartmentUsers = (departmentId: string) => {
  const deptUsers = departmentsIndex[departmentId]?.users || [];
  return deptUsers.filter(user => 
    user.permissions.includes('AUDIT_SITE') &&
    user.status === 'active'
  );
};
 
// Supervisor assignment validation
const validateSupervisorAssignment = (supervisors: SiteSupervisor[]) => {
  // Each department can have max 3 supervisors
  const deptSupervisorCount = {};
  supervisors.forEach(s => {
    deptSupervisorCount[s.departmentID] = 
      (deptSupervisorCount[s.departmentID] || 0) + 1;
  });
  
  return Object.values(deptSupervisorCount).every(count => count <= 3);
};

Validation Rules

Form Validation Logic

const validateSiteForm = (site: Site) => {
  const rules = [
    { field: 'name', required: true, minLength: 3 },
    { field: 'address', required: !organization?.allowCalibration },
    { field: 'timezone', required: true },
    { field: 'coordinates', validator: validateCoordinates },
    { field: 'auditors', validator: (team) => team.length > 0 || siteKey === 'new' }
  ];
  
  return rules.every(rule => {
    if (rule.required && !site[rule.field]) return false;
    if (rule.minLength && site[rule.field]?.length < rule.minLength) return false;
    if (rule.validator && !rule.validator(site[rule.field])) return false;
    return true;
  });
};

Coordinate Validation

const isValidCoordinate = (coord: string) => {
  const num = parseFloat(coord);
  return !isNaN(num) && isFinite(num);
};
 
const validateLatitude = (lat: number) => lat >= -90 && lat <= 90;
const validateLongitude = (lng: number) => lng >= -180 && lng <= 180;

Site Save Process

sequenceDiagram
    participant U as User
    participant C as Component
    participant V as Validator
    participant A as API
    participant R as Redux
    
    U->>C: Click Save
    C->>V: Validate Form
    V-->>C: Validation Result
    
    alt Validation Fails
        C->>U: Show Errors
    else Validation Passes
        C->>A: Submit Site Data
        A-->>C: API Response
        
        alt API Success
            C->>R: Update State
            R-->>C: State Updated
            C->>U: Show Success
        else API Error
            C->>U: Show Error
        end
    end

Data Models

Site Interface

interface Site {
  siteID: string;
  name: string;
  subtitle?: string;
  address: string;
  locationName: string;
  coordinates: {
    latitude: number;
    longitude: number;
  };
  country: string;
  province: string;
  city: string;
  timezone: string;
  utcOffset: number;
  radius: 'default' | 'small' | 'medium' | 'large' | 'custom';
  radiusCustom?: number;
  team: string[]; // Auditor IDs
  owner: string; // Owner ID
  supervisors: SiteSupervisor[];
  departments: Record<string, boolean>;
  departmentList: string[];
  issueDefaultAssignedUser?: string;
  emailTargets: string[];
  createdAt: string;
  createdBy: string;
  updatedAt: string;
  updatedBy: string;
}

Site Supervisor Interface

interface SiteSupervisor {
  departmentID: string;
  userID: string;
}
 
interface PopulatedSiteSupervisor extends SiteSupervisor {
  user: {
    uid: string;
    displayName: string;
    email: string;
  };
  department: {
    departmentID: string;
    name: string;
  };
}

State Management

Redux Store Structure

interface SitesState {
  list: Site[];
  selectedSite: Site | null;
  loading: {
    list: boolean;
    create: boolean;
    update: boolean;
    delete: boolean;
  };
  modalVisible: {
    bulk: boolean;
    schedule: boolean;
    bulkSchedule: boolean;
    bulkScheduleEdit: boolean;
    confirmBlockSite: boolean;
  };
  filters: {
    department: string[];
    status: 'active' | 'blocked' | 'all';
    search: string;
  };
  pagination: {
    page: number;
    limit: number;
    total: number;
  };
}

Redux Actions

// Site CRUD actions
const siteActions = {
  FETCH_SITES_REQUEST: 'FETCH_SITES_REQUEST',
  FETCH_SITES_SUCCESS: 'FETCH_SITES_SUCCESS',
  FETCH_SITES_FAILURE: 'FETCH_SITES_FAILURE',
  
  CREATE_SITE_REQUEST: 'CREATE_SITE_REQUEST',
  CREATE_SITE_SUCCESS: 'CREATE_SITE_SUCCESS',
  CREATE_SITE_FAILURE: 'CREATE_SITE_FAILURE',
  
  UPDATE_SITE_REQUEST: 'UPDATE_SITE_REQUEST',
  UPDATE_SITE_SUCCESS: 'UPDATE_SITE_SUCCESS',
  UPDATE_SITE_FAILURE: 'UPDATE_SITE_FAILURE',
  
  SET_SELECTED_SITE: 'SET_SELECTED_SITE',
  CLEAR_SITE_STATE: 'CLEAR_SITE_STATE'
};

Security and Permissions

Access Control

const sitePermissions = {
  VIEW: 'ADMIN_SITES_VIEW',
  CREATE: 'ADMIN_SITES_CREATE',
  EDIT: 'ADMIN_SITES_EDIT',
  DELETE: 'ADMIN_SITES_DELETE',
  BULK_OPERATIONS: 'ADMIN_SITES_BULK'
};
 
// Permission checks in components
const hasPermission = (permission: string) => {
  return userPermissions.includes(permission);
};

Role-Based Feature Access

const featureGates = {
  allowCalibration: organization?.allowCalibration,
  bulkOperations: featureAccess.BULK_OPERATIONS,
  issueTracker: featureAccess.ISSUE_TRACKER,
  advancedScheduling: featureAccess.ADVANCED_SCHEDULING
};

Additional Site Components

SitesManager Component

Location: src/components/sites/SitesManager/SitesManager.tsx

The SitesManager serves as the main container for site management operations:

Features

  • Site Listing: Paginated table view with sorting and filtering
  • Search Functionality: Real-time search across site names and locations
  • Department Filtering: Filter sites by associated departments
  • Status Management: Toggle between active and blocked sites
  • Bulk Operations: Multi-select for bulk actions

Implementation Details

const SitesManager = ({ itemListLimit }: { itemListLimit: number }) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [selectedDepartments, setSelectedDepartments] = useState<string[]>([]);
  const [statusFilter, setStatusFilter] = useState<'active' | 'blocked' | 'all'>('active');
  
  // Pagination logic
  const [currentPage, setCurrentPage] = useState(1);
  const [totalPages, setTotalPages] = useState(0);
  
  // Site filtering logic
  const filteredSites = useMemo(() => {
    return sites.filter(site => {
      const matchesSearch = site.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
                           site.address.toLowerCase().includes(searchTerm.toLowerCase());
      const matchesDepartment = selectedDepartments.length === 0 ||
                               selectedDepartments.some(dept => site.departments[dept]);
      const matchesStatus = statusFilter === 'all' ||
                           (statusFilter === 'active' && !site.blocked) ||
                           (statusFilter === 'blocked' && site.blocked);
      
      return matchesSearch && matchesDepartment && matchesStatus;
    });
  }, [sites, searchTerm, selectedDepartments, statusFilter]);
};

Site Bulk Operations

SitesBulkModal Component

Location: src/components/sites/SitesBulkModal/SitesBulkModal.tsx

Handles bulk site creation via Excel upload:

Excel Template Structure
const excelTemplate = {
  requiredColumns: [
    'Site Name',
    'Address',
    'Country',
    'Province',
    'City',
    'Latitude',
    'Longitude',
    'Timezone',
    'Department'
  ],
  optionalColumns: [
    'Subtitle',
    'Radius',
    'Email Targets',
    'Owner',
    'Auditors'
  ]
};
Validation Logic
const validateBulkSites = (data: any[]) => {
  const errors: string[] = [];
  
  data.forEach((row, index) => {
    // Required field validation
    if (!row['Site Name']) {
      errors.push(`Row ${index + 1}: Site Name is required`);
    }
    
    // Coordinate validation
    const lat = parseFloat(row['Latitude']);
    const lng = parseFloat(row['Longitude']);
    if (isNaN(lat) || lat < -90 || lat > 90) {
      errors.push(`Row ${index + 1}: Invalid latitude`);
    }
    if (isNaN(lng) || lng < -180 || lng > 180) {
      errors.push(`Row ${index + 1}: Invalid longitude`);
    }
    
    // Department validation
    if (!departments[row['Department']]) {
      errors.push(`Row ${index + 1}: Invalid department`);
    }
  });
  
  return errors;
};

SiteBulkScheduleModal Component

Location: src/components/sites/SiteBulkScheduleModal/SiteBulkScheduleModal.tsx

Manages bulk schedule assignment to multiple sites:

Schedule Validation Rules
const scheduleValidationRules = {
  timeSlots: {
    minimum: 1,
    maximum: 8,
    duration: 30, // minutes
    interval: 15  // minutes between slots
  },
  
  recurrence: {
    types: ['daily', 'weekly', 'monthly', 'custom'],
    weekdays: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
    maxAdvanceDays: 365
  },
  
  auditorAssignment: {
    maxSitesPerAuditor: 10,
    workingHours: {
      start: '06:00',
      end: '22:00'
    }
  }
};

Site Scheduling System

Schedule Data Model

interface SiteSchedule {
  scheduleID: string;
  siteID: string;
  questionnaireID: string;
  auditorID: string;
  supervisorID?: string;
  
  // Timing information
  scheduledDate: string;
  scheduledTime: string;
  duration: number; // minutes
  
  // Recurrence pattern
  recurrence: {
    type: 'none' | 'daily' | 'weekly' | 'monthly' | 'custom';
    interval: number;
    endDate?: string;
    weekdays?: number[]; // 0-6, Sunday to Saturday
    monthlyPattern?: 'date' | 'weekday';
  };
  
  // Status tracking
  status: 'scheduled' | 'in_progress' | 'completed' | 'missed' | 'cancelled';
  actualStartTime?: string;
  actualEndTime?: string;
  completionPercentage: number;
  
  // Metadata
  createdAt: string;
  createdBy: string;
  updatedAt: string;
  updatedBy: string;
}

Schedule Conflict Detection

const detectScheduleConflicts = (
  newSchedule: SiteSchedule,
  existingSchedules: SiteSchedule[]
) => {
  const conflicts: ScheduleConflict[] = [];
  
  existingSchedules.forEach(existing => {
    // Check auditor availability
    if (existing.auditorID === newSchedule.auditorID) {
      const timeOverlap = checkTimeOverlap(
        existing.scheduledTime,
        existing.duration,
        newSchedule.scheduledTime,
        newSchedule.duration
      );
      
      if (timeOverlap) {
        conflicts.push({
          type: 'auditor_conflict',
          conflictingScheduleID: existing.scheduleID,
          message: `Auditor ${existing.auditorID} already scheduled at this time`
        });
      }
    }
    
    // Check site availability
    if (existing.siteID === newSchedule.siteID) {
      const dateMatch = existing.scheduledDate === newSchedule.scheduledDate;
      const timeOverlap = checkTimeOverlap(
        existing.scheduledTime,
        existing.duration,
        newSchedule.scheduledTime,
        newSchedule.duration
      );
      
      if (dateMatch && timeOverlap) {
        conflicts.push({
          type: 'site_conflict',
          conflictingScheduleID: existing.scheduleID,
          message: `Site already has scheduled audit at this time`
        });
      }
    }
  });
  
  return conflicts;
};

Site Groups Management

SiteGroupManager Component

Provides hierarchical organization of sites:

interface SiteGroup {
  groupID: string;
  name: string;
  description?: string;
  parentGroupID?: string;
  departmentID: string;
  siteIDs: string[];
  
  // Hierarchy helpers
  level: number;
  path: string[]; // Array of parent group IDs
  
  // Permissions
  managerIDs: string[];
  permissions: {
    viewSites: boolean;
    editSites: boolean;
    manageSites: boolean;
    viewReports: boolean;
  };
  
  createdAt: string;
  createdBy: string;
  updatedAt: string;
  updatedBy: string;
}

Group Hierarchy Logic

const buildGroupHierarchy = (groups: SiteGroup[]) => {
  const groupMap = new Map<string, SiteGroup>();
  const rootGroups: SiteGroup[] = [];
  
  // First pass: create map
  groups.forEach(group => {
    groupMap.set(group.groupID, { ...group, children: [] });
  });
  
  // Second pass: build hierarchy
  groups.forEach(group => {
    if (group.parentGroupID) {
      const parent = groupMap.get(group.parentGroupID);
      if (parent) {
        parent.children.push(groupMap.get(group.groupID));
      }
    } else {
      rootGroups.push(groupMap.get(group.groupID));
    }
  });
  
  return rootGroups;
};

Activity History System

Activity Tracking

All site operations are tracked for audit trails:

interface SiteActivity {
  activityID: string;
  siteID: string;
  userID: string;
  
  // Activity details
  action: 'created' | 'updated' | 'deleted' | 'blocked' | 'unblocked' | 'schedule_added' | 'schedule_removed';
  description: string;
  
  // Change tracking
  changes: {
    field: string;
    oldValue: any;
    newValue: any;
  }[];
  
  // Context
  ipAddress: string;
  userAgent: string;
  sessionID: string;
  
  timestamp: string;
}

Activity History Hook

const useSiteActivityHistory = ({ siteID, limit = 50 }) => {
  const [activities, setActivities] = useState<SiteActivity[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  const fetchActivities = useCallback(async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetchSiteActivityHistory({
        siteID,
        limit,
        orderBy: 'timestamp',
        order: 'desc'
      });
      
      setActivities(response.data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }, [siteID, limit]);
  
  useEffect(() => {
    if (siteID && siteID !== 'new') {
      fetchActivities();
    }
  }, [fetchActivities, siteID]);
  
  return { activities, loading, error, refetch: fetchActivities };
};

Performance Optimization

Virtual Scrolling for Large Lists

const VirtualizedSiteList = ({ sites, itemHeight = 60 }) => {
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });
  const containerRef = useRef<HTMLDivElement>(null);
  
  const handleScroll = useCallback((e: Event) => {
    const scrollTop = (e.target as HTMLElement).scrollTop;
    const containerHeight = containerRef.current?.clientHeight || 0;
    
    const start = Math.floor(scrollTop / itemHeight);
    const end = Math.min(
      start + Math.ceil(containerHeight / itemHeight) + 5,
      sites.length
    );
    
    setVisibleRange({ start, end });
  }, [itemHeight, sites.length]);
  
  const visibleSites = sites.slice(visibleRange.start, visibleRange.end);
  
  return (
    <div
      ref={containerRef}
      style={{ height: '400px', overflow: 'auto' }}
      onScroll={handleScroll}
    >
      <div style={{ height: sites.length * itemHeight, position: 'relative' }}>
        {visibleSites.map((site, index) => (
          <SiteListItem
            key={site.siteID}
            site={site}
            style={{
              position: 'absolute',
              top: (visibleRange.start + index) * itemHeight,
              height: itemHeight
            }}
          />
        ))}
      </div>
    </div>
  );
};

Debounced Search Implementation

const useDebounceSearch = (initialValue: string, delay: number = 300) => {
  const [searchTerm, setSearchTerm] = useState(initialValue);
  const [debouncedTerm, setDebouncedTerm] = useState(initialValue);
  
  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedTerm(searchTerm);
    }, delay);
    
    return () => {
      clearTimeout(handler);
    };
  }, [searchTerm, delay]);
  
  return [debouncedTerm, setSearchTerm] as const;
};

Error Handling and User Feedback

Error Boundary for Site Components

class SiteErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  
  componentDidCatch(error, errorInfo) {
    // Log error to monitoring service
    console.error('Site component error:', error, errorInfo);
    
    // Track error in analytics
    ReactGA.exception({
      description: error.toString(),
      fatal: false
    });
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>Something went wrong with the site management.</h2>
          <p>Please refresh the page or contact support if the issue persists.</p>
          <button onClick={() => window.location.reload()}>
            Refresh Page
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

Toast Notification System

const showSiteNotification = {
  success: (message: string) => {
    toast.success(message, {
      position: 'top-right',
      autoClose: 3000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true
    });
  },
  
  error: (message: string) => {
    toast.error(message, {
      position: 'top-right',
      autoClose: 5000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true
    });
  },
  
  warning: (message: string) => {
    toast.warning(message, {
      position: 'top-right',
      autoClose: 4000,
      hideProgressBar: false,
      closeOnClick: true,
      pauseOnHover: true
    });
  }
};

Testing Strategy

Unit Test Examples

// Site validation tests
describe('Site Validation', () => {
  test('should validate required fields', () => {
    const invalidSite = {
      name: '',
      address: '',
      coordinates: { latitude: 0, longitude: 0 }
    };
    
    const validation = validateSiteForm(invalidSite);
    expect(validation.isValid).toBe(false);
    expect(validation.errors).toContain('Site name is required');
  });
  
  test('should validate coordinates', () => {
    const invalidCoords = { latitude: 91, longitude: 181 };
    expect(validateLatitude(invalidCoords.latitude)).toBe(false);
    expect(validateLongitude(invalidCoords.longitude)).toBe(false);
  });
  
  test('should validate custom radius', () => {
    expect(validateCustomRadius(0)).toBe(false);
    expect(validateCustomRadius(50)).toBe(true);
    expect(validateCustomRadius(1001)).toBe(false);
  });
});

Integration Test Examples

// Site creation flow test
describe('Site Creation Integration', () => {
  test('should create site with valid data', async () => {
    const siteData = {
      name: 'Test Site',
      address: '123 Test Street',
      coordinates: { latitude: 40.7128, longitude: -74.0060 },
      timezone: 'America/New_York',
      utcOffset: -300,
      departments: { 'dept1': true }
    };
    
    const mockApiResponse = { siteID: 'site123', ...siteData };
    jest.spyOn(siteApi, 'createSite').mockResolvedValue(mockApiResponse);
    
    const { getByTestId } = render(<SiteDetailsContainer siteKey="new" />);
    
    // Fill form
    fireEvent.change(getByTestId('site-name'), { target: { value: siteData.name } });
    fireEvent.click(getByTestId('submit-button'));
    
    await waitFor(() => {
      expect(siteApi.createSite).toHaveBeenCalledWith(
        expect.objectContaining(siteData)
      );
    });
  });
});

Accessibility Features

ARIA Labels and Roles

const AccessibleSiteForm = () => {
  return (
    <form role="form" aria-labelledby="site-form-title">
      <h2 id="site-form-title">Site Information</h2>
      
      <div className="form-group">
        <label htmlFor="site-name" className="required">
          Site Name
        </label>
        <input
          id="site-name"
          type="text"
          aria-required="true"
          aria-describedby="site-name-error"
          aria-invalid={!isValid}
        />
        <div id="site-name-error" aria-live="polite">
          {errors.name && <span role="alert">{errors.name}</span>}
        </div>
      </div>
      
      <div className="form-group">
        <fieldset>
          <legend>Location Type</legend>
          <label>
            <input
              type="radio"
              name="location-type"
              value="address"
              aria-describedby="location-type-description"
            />
            Address
          </label>
          <label>
            <input
              type="radio"
              name="location-type"
              value="coordinates"
              aria-describedby="location-type-description"
            />
            Coordinates
          </label>
          <div id="location-type-description">
            Choose how to specify the site location
          </div>
        </fieldset>
      </div>
    </form>
  );
};

Keyboard Navigation

const useKeyboardNavigation = () => {
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      switch (e.key) {
        case 'Enter':
          if (e.ctrlKey || e.metaKey) {
            // Ctrl+Enter to save
            handleSave();
            e.preventDefault();
          }
          break;
        case 'Escape':
          // ESC to cancel
          handleCancel();
          e.preventDefault();
          break;
        case 's':
          if (e.ctrlKey || e.metaKey) {
            // Ctrl+S to save
            handleSave();
            e.preventDefault();
          }
          break;
      }
    };
    
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, []);
};

Internationalization (i18n)

Translation Keys

const siteTranslations = {
  en: {
    'site.name.label': 'Site Name',
    'site.name.placeholder': 'Enter site name',
    'site.address.label': 'Address',
    'site.coordinates.label': 'Coordinates',
    'site.timezone.label': 'Timezone',
    'site.departments.label': 'Departments',
    'site.auditors.label': 'Auditors',
    'site.supervisors.label': 'Supervisors',
    'site.save.button': 'Save Site',
    'site.cancel.button': 'Cancel',
    'site.validation.name.required': 'Site name is required',
    'site.validation.address.required': 'Address is required',
    'site.validation.coordinates.invalid': 'Invalid coordinates'
  },
  id: {
    'site.name.label': 'Nama Situs',
    'site.name.placeholder': 'Masukkan nama situs',
    'site.address.label': 'Alamat',
    'site.coordinates.label': 'Koordinat',
    'site.timezone.label': 'Zona Waktu',
    'site.departments.label': 'Departemen',
    'site.auditors.label': 'Auditor',
    'site.supervisors.label': 'Supervisor',
    'site.save.button': 'Simpan Situs',
    'site.cancel.button': 'Batal',
    'site.validation.name.required': 'Nama situs diperlukan',
    'site.validation.address.required': 'Alamat diperlukan',
    'site.validation.coordinates.invalid': 'Koordinat tidak valid'
  }
};

Usage in Components

const SiteForm = () => {
  const { t } = useTranslation();
  
  return (
    <div>
      <label>{t('site.name.label')}</label>
      <input placeholder={t('site.name.placeholder')} />
      
      {errors.name && (
        <span className="error">
          {t('site.validation.name.required')}
        </span>
      )}
    </div>
  );
};

Component Structure

// Standard component file structure
src/components/sites/
├── SiteDetails/
│   ├── SiteDetailsContainer.tsx        // Main container component
│   ├── SiteDetails.tsx                 // Presentation component
│   ├── components/
│   │   ├── FormControl.tsx             // Form control wrapper
│   │   ├── Label.tsx                   // Label component
│   │   └── AssignedDepartment.tsx      // Department assignment
│   ├── hooks/
│   │   ├── useRadiusList.ts           // Radius options hook
│   │   └── useSiteActivityHistory.ts   // Activity history hook
│   └── SiteDetails.test.tsx           // Component tests

Separation of Concerns

// Container Component (SiteDetailsContainer.tsx)
// Handles: State management, API calls, business logic
const SiteDetailsContainer = (props) => {
  // State and effects
  const [site, setSite] = useState(null);
  const [validation, setValidation] = useState([]);
  
  // Business logic
  const handleSave = useCallback(() => {
    // Validation and API calls
  }, []);
  
  // Render presentation component
  return (
    <SiteDetails
      site={site}
      onSave={handleSave}
      validation={validation}
      {...otherProps}
    />
  );
};
 
// Presentation Component (SiteDetails.tsx)
// Handles: UI rendering, user interactions
const SiteDetails = ({ site, onSave, validation }) => {
  return (
    <form onSubmit={onSave}>
      {/* UI elements */}
    </form>
  );
};

useGoogleMaps Hook

const useGoogleMaps = (apiKey: string) => {
  const [isLoaded, setIsLoaded] = useState(false);
  const [error, setError] = useState<string | null>(null);
  
  useEffect(() => {
    if (window.google) {
      setIsLoaded(true);
      return;
    }
    
    const script = document.createElement('script');
    script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places`;
    script.async = true;
    
    script.onload = () => setIsLoaded(true);
    script.onerror = () => setError('Failed to load Google Maps');
    
    document.head.appendChild(script);
    
    return () => {
      document.head.removeChild(script);
    };
  }, [apiKey]);
  
  return { isLoaded, error };
};

useSiteForm Hook

const useSiteForm = (initialSite: Site | null) => {
  const [site, setSite] = useState<Site | null>(initialSite);
  const [hasChanges, setHasChanges] = useState(false);
  const [errors, setErrors] = useState<Record<string, string>>({});
  
  const updateField = useCallback((field: string, value: any) => {
    setSite(prevSite => ({
      ...prevSite,
      [field]: value
    }));
    setHasChanges(true);
    
    // Clear field error if exists
    if (errors[field]) {
      setErrors(prev => {
        const newErrors = { ...prev };
        delete newErrors[field];
        return newErrors;
      });
    }
  }, [errors]);
  
  const validate = useCallback(() => {
    const newErrors: Record<string, string> = {};
    
    if (!site?.name) {
      newErrors.name = 'Site name is required';
    }
    
    if (!site?.address && !site?.coordinates) {
      newErrors.address = 'Address or coordinates are required';
    }
    
    // Additional validations...
    
    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  }, [site]);
  
  return {
    site,
    setSite,
    hasChanges,
    setHasChanges,
    errors,
    updateField,
    validate
  };
};

Redux Slice Implementation

// sites.slice.ts
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
 
export const fetchSites = createAsyncThunk(
  'sites/fetchSites',
  async (params: FetchSitesParams) => {
    const response = await siteApi.fetchSites(params);
    return response.data;
  }
);
 
export const createSite = createAsyncThunk(
  'sites/createSite',
  async (siteData: CreateSiteRequest) => {
    const response = await siteApi.createSite(siteData);
    return response.data;
  }
);
 
const sitesSlice = createSlice({
  name: 'sites',
  initialState: {
    list: [],
    selectedSite: null,
    loading: {
      fetch: false,
      create: false,
      update: false
    },
    error: null
  },
  reducers: {
    setSelectedSite: (state, action) => {
      state.selectedSite = action.payload;
    },
    clearError: (state) => {
      state.error = null;
    }
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchSites.pending, (state) => {
        state.loading.fetch = true;
      })
      .addCase(fetchSites.fulfilled, (state, action) => {
        state.loading.fetch = false;
        state.list = action.payload;
      })
      .addCase(fetchSites.rejected, (state, action) => {
        state.loading.fetch = false;
        state.error = action.error.message;
      });
  }
});
 
export const { setSelectedSite, clearError } = sitesSlice.actions;
export default sitesSlice.reducer;

Service Layer Architecture

// siteService.ts
class SiteService {
  private baseURL = '/api/sites';
  
  async fetchSites(params: FetchSitesParams): Promise<Site[]> {
    const response = await this.request('GET', this.baseURL, { params });
    return response.data;
  }
  
  async createSite(siteData: CreateSiteRequest): Promise<Site> {
    const response = await this.request('POST', this.baseURL, { data: siteData });
    return response.data;
  }
  
  async updateSite(siteId: string, siteData: UpdateSiteRequest): Promise<Site> {
    const response = await this.request('PUT', `${this.baseURL}/${siteId}`, { data: siteData });
    return response.data;
  }
  
  private async request(method: string, url: string, options: RequestOptions = {}) {
    const config = {
      method,
      url,
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...await this.getAuthHeaders(),
        ...options.headers
      }
    };
    
    try {
      const response = await axios(config);
      return response.data;
    } catch (error) {
      throw this.handleError(error);
    }
  }
  
  private async getAuthHeaders() {
    const user = firebase.auth().currentUser;
    if (!user) throw new Error('User not authenticated');
    
    const token = await user.getIdToken();
    return { Authorization: `Bearer ${token}` };
  }
  
  private handleError(error: any) {
    if (error.response) {
      // Server responded with error status
      return new Error(error.response.data.message || 'Server error');
    } else if (error.request) {
      // Request made but no response
      return new Error('Network error');
    } else {
      // Something else happened
      return new Error(error.message);
    }
  }
}
 
export const siteService = new SiteService();

Memoization Patterns

// Memoized selectors
const selectFilteredSites = createSelector(
  [
    (state: RootState) => state.sites.list,
    (state: RootState) => state.sites.filters.search,
    (state: RootState) => state.sites.filters.departments,
    (state: RootState) => state.sites.filters.status
  ],
  (sites, search, departments, status) => {
    return sites.filter(site => {
      const matchesSearch = !search || 
        site.name.toLowerCase().includes(search.toLowerCase()) ||
        site.address.toLowerCase().includes(search.toLowerCase());
      
      const matchesDepartments = departments.length === 0 ||
        departments.some(dept => site.departments[dept]);
      
      const matchesStatus = status === 'all' ||
        (status === 'active' && !site.blocked) ||
        (status === 'blocked' && site.blocked);
      
      return matchesSearch && matchesDepartments && matchesStatus;
    });
  }
);
 
// Memoized components
const SiteListItem = React.memo(({ site, onEdit, onDelete }) => {
  return (
    <div className="site-item">
      <h3>{site.name}</h3>
      <p>{site.address}</p>
      <div className="actions">
        <button onClick={() => onEdit(site.siteID)}>Edit</button>
        <button onClick={() => onDelete(site.siteID)}>Delete</button>
      </div>
    </div>
  );
});

Conclusion

The Site Creation and Edit module represents a comprehensive solution for managing audit sites within the Nimbly platform. Key achievements include:

Technical Excellence

  • Modern Architecture: React hooks, TypeScript, and clean code patterns
  • Performance Optimization: Virtual scrolling, lazy loading, and memoization
  • Accessibility: WCAG compliance with ARIA labels and keyboard navigation
  • Internationalization: Multi-language support for global deployment

Business Value

  • Streamlined Operations: Intuitive UI reduces training time and errors
  • Scalability: Handles thousands of sites with efficient pagination and filtering
  • Integration: Seamless connection with scheduling, reporting, and analytics
  • Compliance: Comprehensive audit trails and permission controls

Future-Ready Design

  • Extensible Architecture: Plugin-based components for easy feature additions
  • API-First Design: RESTful APIs enable mobile and third-party integrations
  • Cloud-Native: Optimized for cloud deployment and auto-scaling
  • Data-Driven: Analytics integration for continuous improvement

This documentation serves as both a technical reference and implementation guide for developers working on the Nimbly audit administration platform. The site management system demonstrates enterprise-grade software development practices while maintaining usability and performance at scale.