Bulk Schedule Module - Technical Documentation

Table of Contents

  1. Architecture & Overview
  2. Core Domain Models
  3. Business Logic & Services
  4. Data Access Layer
  5. User Interface Components
  6. Features Deep Dive
  7. Integration & APIs
  8. Internationalization & Localization
  9. Package Dependencies
  10. File Structure Reference

Architecture & Overview

Introduction

The bulk-schedule module is a comprehensive scheduling management system built within the Nimbly admin application. It enables administrators to create, manage, and deploy schedules across multiple sites and departments in bulk, significantly reducing the administrative overhead of individual schedule creation.

The module consists of two primary features:

  1. Bulk Schedule Creation - A multi-step wizard for creating schedules across multiple site-department combinations
  2. Draft Schedule Management - A complete CRUD system for managing saved schedule drafts

Business Context

The bulk-schedule module addresses the operational need for large organizations to deploy consistent scheduling patterns across multiple locations efficiently. Key business drivers include:

  • Scale Efficiency: Create hundreds of schedules with a single configuration
  • Consistency: Ensure uniform scheduling patterns across organizational units
  • Flexibility: Support various schedule types (daily, weekly, monthly, custom)
  • Override Capability: Allow site-specific customizations while maintaining global defaults
  • Draft Management: Enable iterative schedule design and approval workflows

High-Level Architecture

The module follows Clean Architecture principles with clear separation between domain logic, application services, and infrastructure concerns:

graph TB
    subgraph "UI Layer"
        A[Wizard Components] 
        B[Draft Management]
        C[Shared Components]
    end
    
    subgraph "Application Layer"
        D[Bulk Schedule Service]
        E[Draft Schedule Service]
        F[State Management - Jotai]
    end
    
    subgraph "Domain Layer"
        G[Schedule Entities]
        H[Draft Entities]
        I[Business Rules]
    end
    
    subgraph "Infrastructure Layer"
        J[Bulk Schedule Repository]
        K[Draft Schedule Repository]
        L[API Client]
    end
    
    subgraph "External Systems"
        M[Nimbly API]
        N[Questionnaire Service]
        O[Site Management]
        P[User Management]
    end
    
    A --> D
    B --> E
    C --> D
    C --> E
    
    D --> G
    D --> J
    E --> H
    E --> K
    
    J --> L
    K --> L
    L --> M
    L --> N
    L --> O
    L --> P
    
    F --> A
    F --> B

Technology Stack

The module leverages modern web technologies and follows the established patterns of the Nimbly admin application:

Frontend Technologies

  • Next.js 15 with App Router - File-based routing and server-side rendering
  • TypeScript - Type safety throughout the application
  • React 18 - Component-based UI framework
  • Tailwind CSS - Utility-first CSS framework
  • shadcn/ui - High-quality component library

State Management

  • TanStack Query (React Query) - Server state management with caching
  • Jotai - Atomic state management for client-side state
  • React Hook Form - Form state and validation management

Internationalization

  • Lingui - Internationalization framework with compile-time optimization
  • Multiple Language Support - English, Spanish, Portuguese, Indonesian, Korean, Thai, Chinese (Mandarin)

Development & Testing

  • Vitest - Unit testing framework
  • TypeScript ESLint - Code quality and consistency
  • Prettier - Code formatting

Directory Structure

The module follows the feature-first architecture pattern established in the codebase:

src/app/admin/bulk-schedule/
├── layout.tsx                          # Tab navigation layout
├── page.tsx                           # Root redirect page
├── _domain/                           # Domain entities and DTOs
│   ├── schedule-entities.ts           # Core business entities
│   ├── bulk-schedule-dtos.ts          # API request/response types
│   ├── draft-schedule-entities.ts     # Draft-specific entities
│   └── draft-schedule-dtos.ts         # Draft API types
├── _lib/                              # Business logic and utilities
│   ├── bulk-schedule-service.ts       # Core business logic
│   ├── bulk-schedule-repository.ts    # Data access layer
│   ├── bulk-schedule-queries.ts       # TanStack Query options
│   ├── bulk-schedule-hooks.ts         # Jotai-based state hooks
│   ├── bulk-schedule-atoms.ts         # Jotai atom definitions
│   ├── bulk-schedule-utils.ts         # Utility functions
│   ├── draft-schedule-service.ts      # Draft business logic
│   ├── draft-schedule-repository.ts   # Draft data access
│   ├── draft-schedule-queries.ts      # Draft query options
│   ├── draft-schedule-transformer.ts  # Data transformation
│   ├── use-bulk-schedule-navigation.ts # Navigation utilities
│   ├── use-reset-bulk-schedule.ts     # State reset utilities
│   └── use-schedule-data-cleaning.ts  # Data cleaning utilities
├── create/                            # Bulk schedule creation wizard
│   ├── page.tsx                       # Wizard entry point
│   ├── loading.tsx                    # Loading state
│   └── _components/                   # Wizard components
│       ├── bulk-schedule-wizard.tsx   # Main wizard orchestrator
│       ├── configure-schedule.tsx     # Step 1: Configuration
│       ├── global-advanced-settings.tsx # Step 2: Advanced settings
│       ├── site-department-assignment.tsx # Step 3: Site assignment
│       ├── schedule-preview.tsx       # Step 4: Preview
│       ├── questionnaire-selection.tsx # Questionnaire picker
│       ├── save-draft-modal.tsx       # Draft saving modal
│       ├── wizard/                    # Wizard infrastructure
│       ├── schedule/                  # Schedule configuration components
│       ├── assignment/                # Assignment components
│       ├── questionnaire/             # Questionnaire components
│       └── shared/                    # Shared wizard components
├── drafts/                            # Draft management
│   ├── page.tsx                       # Draft list page
│   ├── _components/                   # Draft components
│   │   ├── draft-schedule-table.tsx   # Desktop table view
│   │   ├── draft-schedule-card.tsx    # Mobile card view
│   │   ├── draft-actions.tsx          # Draft action buttons
│   │   ├── draft-detail-modal.tsx     # Draft details modal
│   │   ├── search-filters.tsx         # Search and filtering
│   │   ├── draft-pagination.tsx       # Pagination component
│   │   └── draft-skeleton.tsx         # Loading skeleton
│   └── _lib/                          # Draft-specific utilities
│       ├── draft-schedule-hooks.ts    # Draft state hooks
│       └── use-questionnaire-name.ts  # Questionnaire name resolver
└── locales/                           # Internationalization files
    ├── bulk-schedule/                 # Locale-specific translations
    │   ├── en.po                      # English translations
    │   ├── es.po                      # Spanish translations
    │   ├── pt.po                      # Portuguese translations
    │   ├── id.po                      # Indonesian translations
    │   ├── ko.po                      # Korean translations
    │   ├── th.po                      # Thai translations
    │   └── cmn.po                     # Chinese (Mandarin) translations
    └── ...

Design Principles

The bulk-schedule module adheres to several key design principles:

1. Clean Architecture

  • Separation of Concerns: Clear boundaries between UI, business logic, and data access
  • Dependency Inversion: High-level modules don’t depend on low-level modules
  • Domain-First Design: Business rules are isolated from infrastructure concerns

2. Feature-First Organization

  • Co-location: Related functionality is kept together
  • Gradual Extraction: Shared code is extracted only when genuinely reused
  • Context Boundaries: Clear feature boundaries prevent coupling

3. State Management Strategy

  • Server State: TanStack Query for API data with intelligent caching
  • Client State: Jotai atoms for granular, reactive state management
  • Form State: React Hook Form for complex form interactions
  • Derived State: Computed values through Jotai derived atoms

4. Progressive Enhancement

  • Mobile-First: Responsive design starting from mobile constraints
  • Accessibility: WCAG 2.1 AA compliance through semantic HTML and ARIA
  • Performance: Code splitting, lazy loading, and optimized queries

5. Error Handling

  • Graceful Degradation: Fallback UI states for error conditions
  • Monitoring Integration: Sentry integration for error tracking
  • User Experience: Clear error messages and recovery paths

Key Architectural Patterns

Repository Pattern

Each feature uses a repository class extending BaseRepository for standardized data access patterns, monitoring, and error handling.

Service Layer Pattern

Business logic is encapsulated in service classes extending BaseService, providing transaction boundaries and business rule enforcement.

Query Options Pattern

TanStack Query configuration is centralized in query options factories, providing consistent caching strategies and query key management.

Atomic State Management

Jotai atoms provide granular, composable state management with automatic dependency tracking and minimal re-renders.

Component Composition

React components are composed through props and children patterns, avoiding complex inheritance hierarchies.


Core Domain Models

Overview

The bulk-schedule module is built around several core domain entities that represent the business concepts and relationships within the scheduling system. These entities are defined in TypeScript interfaces that provide strong typing and clear contracts throughout the application.

Schedule Entities

BulkScheduleConfig

The central configuration entity representing a complete bulk schedule setup:

File: src/app/admin/bulk-schedule/_domain/schedule-entities.ts:7-37

export type BulkScheduleConfig = {
  // Step 1: Questionnaire
  questionnaireIndexId: string;
  questionnaireName?: string;
  questionCount?: number;
 
  // Step 2: Basic Schedule
  scheduleType: ScheduleTypes;
  isRecurring: boolean;
  startDate: string;
  endDate: string;
  isAdhocOnly?: boolean;
  allowAdhoc?: boolean;
  isEom?: boolean; // End of month scheduling
 
  // For recurring schedules
  repeatingType?: RepeatingTypes;
  daysOfWeek?: number[];
  datesOfMonth?: number[];
  occurenceNumber?: number;
  repeatingEndDate?: Date;
 
  // For custom schedules
  customDates?: string[];
 
  // Step 3: Global Advanced Settings
  globalSettings: ScheduleAdvancedSettings;
 
  // Step 4: Site-Department Mappings
  assignments: SiteDepartmentAssignment[];
};

Key Properties:

  • questionnaireIndexId: Links to the questionnaire to be used for audits
  • scheduleType: Determines recurrence pattern (daily, weekly, monthly, custom)
  • dateRange: Defines the active period for the schedule
  • globalSettings: Default configuration applied to all assignments
  • assignments: Specific site-department combinations with optional overrides

ScheduleAdvancedSettings

Comprehensive configuration for schedule behavior and features:

File: src/app/admin/bulk-schedule/_domain/schedule-entities.ts:39-78

export type ScheduleAdvancedSettings = {
  // [[User Roles/auditor|Auditor]] settings
  assignedAuditor?: string;
  auditors?: string[];
  supervisors?: string[];
  issueOwners?: string[];
  defaultIssueOwner?: string;
  supervisor?: string;
 
  // Time settings
  startTime?: number;
  endTime?: number;
  isEndTimeNextDay?: boolean;
  hasDeadline?: boolean;
  hasStrictTime?: boolean;
  offsetTime?: number;
 
  // Features
  enforceCheckIn?: boolean;
  enforceCheckOut?: boolean;
  signatures?: number;
  selfieSignatures?: boolean;
  selfieTitle?: string;
  allowAdhoc?: boolean;
 
  // Reminders
  reminderTime?: number;
  reminderPeriod?: ReminderPeriod;
 
  // [[Features/Notification/Notification Overview|Notifications]]
  emailTargets?: string[];
 
  // Calendar sync
  withCalendarSync?: boolean;
 
  // Active period settings
  hasActivePeriod?: boolean;
  activePeriodValue?: number;
  activePeriodUnit?: string;
};

Feature Categories:

  • Personnel Management: Auditor assignment, supervision hierarchy
  • Time Configuration: Schedule timing, deadlines, offset calculations
  • Compliance Features: Check-in/out enforcement, signature requirements
  • Notification System: Email alerts, reminder configuration
  • Integration: Calendar sync, active period management

SiteDepartmentAssignment

Represents a specific site-department combination with optional customizations:

File: src/app/admin/bulk-schedule/_domain/schedule-entities.ts:80-93

export type SiteDepartmentAssignment = {
  id: string;
  siteId: string;
  siteName?: string;
  departmentId: string;
  departmentName?: string;
  timezone?: string;
 
  // Optional overrides
  overrides?: Partial<ScheduleAdvancedSettings>;
 
  // UI state
  hasOverrides?: boolean;
};

Override Mechanism: Assignments can override global settings selectively, allowing for site-specific customizations while maintaining organizational consistency.

Draft Entities

DraftSchedule

Complete representation of a saved schedule draft:

File: src/app/admin/bulk-schedule/_domain/draft-schedule-entities.ts:98-109

export type DraftSchedule = {
  _id: string;
  organizationID: string;
  draft: DraftScheduleData;
  createdBy: string;
  updatedBy: string;
  draftName: string;
  draftId: string;
  createdAt: string;
  updatedAt: string;
  __v: number;
};

DraftScheduleData

The actual schedule configuration stored within a draft:

File: src/app/admin/bulk-schedule/_domain/draft-schedule-entities.ts:92-96

export type DraftScheduleData = {
  schedule: Schedule;
  globalAdvanceConfig: GlobalAdvanceConfig;
  siteDeptMapping: SiteDeptMapping[];
};

Preview and Validation Entities

BulkSchedulePreview

Result of schedule preview generation showing conflicts and estimated impact:

File: src/app/admin/bulk-schedule/_domain/schedule-entities.ts:95-101

export type BulkSchedulePreview = {
  totalSchedules: number;
  schedules: SchedulePreviewItem[];
  conflicts: ScheduleConflict[];
  warnings: string[];
  estimatedCreationTime?: number;
};

ScheduleConflict

Represents potential conflicts detected during validation:

File: src/app/admin/bulk-schedule/_domain/schedule-entities.ts:122-131

export type ScheduleConflict = {
  siteId: string;
  siteName?: string;
  departmentId: string;
  departmentName?: string;
  existingScheduleId: string;
  conflictType: 'overlap' | 'duplicate' | 'questionnaire';
  message: string;
  severity: 'error' | 'warning';
};

Wizard State Management

BulkScheduleWizardState

State container for the multi-step wizard process:

File: src/app/admin/bulk-schedule/_domain/schedule-entities.ts:143-149

export type BulkScheduleWizardState = {
  currentStep: number;
  config: Partial<BulkScheduleConfig>;
  stepStatus: Record<string, WizardStepStatus>;
  isDirty: boolean;
  validationErrors: Record<string, string[]>;
};

Step Status Types:

export type WizardStepStatus = 'pending' | 'current' | 'completed' | 'error';

Data Transfer Objects (DTOs)

The module uses comprehensive DTOs for API communication, ensuring type safety between the client and server.

Request DTOs

CreateBulkScheduleDto: Main creation payload

export type CreateBulkScheduleDto = {
  config: BulkScheduleConfig;
  validateOnly?: boolean;
};

GetScheduleConflictsDto: Conflict checking parameters

export type GetScheduleConflictsDto = {
  siteIds: string[];
  departmentIds: string[];
  startDate: string;
  endDate: string;
  questionnaireId: string;
};

Response DTOs

QuestionnairesApiResponseDto: Standardized API response format

export type QuestionnairesApiResponseDto = {
  message: 'SUCCESS';
  data: Questionnaire[];
};

All API responses follow the standard Nimbly API format with message and data properties for consistency.

Business Rules

The domain entities include embedded business rules through TypeScript type constraints and validation functions:

Draft Schedule Business Rules

File: src/app/admin/bulk-schedule/_domain/draft-schedule-entities.ts:126-164

export const DraftScheduleBusinessRules = {
  canEdit: (_draft: DraftSchedule): boolean => {
    return true; // Placeholder for future business logic
  },
 
  canDelete: (_draft: DraftSchedule): boolean => {
    return true; // Placeholder for future business logic
  },
 
  canPublish: (draft: DraftSchedule): boolean => {
    return !!draft.draft.schedule.questionnaireIndexID;
  },
 
  getScheduleTypeLabel: (type: ScheduleType, i18n?: { _: (msg: { id: string }) => string }): string => {
    const labels: Record<ScheduleType, ReturnType<typeof msg>> = {
      daily: msg`Daily`,
      weekly: msg`Weekly`,
      monthly: msg`Monthly`,
      custom: msg`Custom`,
    };
    
    const label = labels[type];
    return label && i18n ? i18n._(label) : type;
  },
 
  formatScheduleTime: (time: number): string => {
    const hours = Math.floor(time / 60);
    const minutes = time % 60;
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
  },
} as const;

Entity Relationships

erDiagram
    BulkScheduleConfig ||--o{ SiteDepartmentAssignment : contains
    BulkScheduleConfig ||--|| ScheduleAdvancedSettings : includes
    SiteDepartmentAssignment ||--o| ScheduleAdvancedSettings : overrides
    
    DraftSchedule ||--|| DraftScheduleData : contains
    DraftScheduleData ||--|| Schedule : includes
    DraftScheduleData ||--|| GlobalAdvanceConfig : includes
    DraftScheduleData ||--o{ SiteDeptMapping : contains
    
    BulkSchedulePreview ||--o{ SchedulePreviewItem : contains
    BulkSchedulePreview ||--o{ ScheduleConflict : identifies
    
    BulkScheduleConfig {
        string questionnaireIndexId
        ScheduleTypes scheduleType
        string startDate
        string endDate
        boolean isRecurring
    }
    
    ScheduleAdvancedSettings {
        number startTime
        number endTime
        string[] auditors
        string[] supervisors
        boolean enforceCheckIn
        number signatures
    }
    
    SiteDepartmentAssignment {
        string id
        string siteId
        string departmentId
        boolean hasOverrides
    }

The domain model provides a solid foundation for the bulk-schedule module, with clear entity boundaries, comprehensive type safety, and embedded business rules that ensure data integrity throughout the application lifecycle.


Business Logic & Services

Overview

The business logic layer implements the core functionality of the bulk-schedule module through service classes that extend the BaseService foundation. These services encapsulate complex business rules, orchestrate data transformations, and provide clean interfaces for the UI layer.

BulkScheduleService

The primary service responsible for bulk schedule operations, validation, and payload transformation.

File: src/app/admin/bulk-schedule/_lib/bulk-schedule-service.ts

Core Operations

Data Retrieval Methods

getQuestionnaires(): Retrieves available questionnaires for schedule creation

async getQuestionnaires(): Promise<Questionnaire[]> {
  return this.withMonitoring('get_questionnaires', 'bulk_schedule', async () => {
    const response = await this.repository.getQuestionnaires();
    if (response.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to fetch questionnaires`));
    }
    return response.data;
  }, 'medium');
}
  • Monitoring: Tracks performance with medium business impact
  • Error Handling: Validates API response format
  • Internationalization: Error messages are localized

getSitesForScheduling(): Retrieves sites with optional filtering

async getSitesForScheduling(params?: GetSitesForSchedulingDto): Promise<SiteCompact[]> {
  return this.withMonitoring('get_sites_for_scheduling', 'bulk_schedule', async () => {
    const response = await this.repository.getSitesForScheduling(params);
    if (response.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to fetch sites`));
    }
    return response.data;
  }, 'medium', {
    has_search: Boolean(params?.search),
    has_department_filter: Boolean(params?.departmentIds?.length),
  });
}
  • Monitoring Context: Captures search and filter usage patterns
  • Flexible Filtering: Supports search terms and department-based filtering
Validation and Conflict Detection

checkScheduleConflicts(): Detects potential scheduling conflicts

async checkScheduleConflicts(params: GetScheduleConflictsDto): Promise<{
  conflicts: Array<{
    siteId: string;
    siteName?: string;
    departmentId: string;
    departmentName?: string;
    existingScheduleId: string;
    conflictType: 'overlap' | 'duplicate' | 'questionnaire';
    message: string;
    severity: 'error' | 'warning';
  }>;
  hasConflicts: boolean;
}> {
  return this.withMonitoring('check_schedule_conflicts', 'bulk_schedule', async () => {
    if (!params.siteIds.length || !params.departmentIds.length) {
      return { conflicts: [], hasConflicts: false };
    }
    
    return this.repository.checkScheduleConflicts(params);
  }, 'high', {
    site_count: params.siteIds.length,
    department_count: params.departmentIds.length,
  });
}
  • Early Validation: Prevents API calls when parameters are empty
  • High Priority Monitoring: Conflict detection is business-critical
  • Detailed Conflict Information: Provides actionable conflict resolution data
Payload Transformation

transformToPayload(): Converts UI configuration to API format

transformToPayload(
  config: BulkScheduleConfig, 
  organizationID?: string, 
  userID?: string
): GeneralSchedulePayload {
  const now = new Date().toISOString();
  const startTime = config.globalSettings.startTime || 0;
  const endTime = config.globalSettings.endTime || 0;
  
  // Complex transformation logic...
  return {
    data: {
      schedule: {
        isEom: config.isEom || false,
        startDate: config.startDate || '',
        endDate: config.endDate || '',
        startTime,
        endTime,
        // ... detailed field mapping
      },
      globalAdvanceConfig: {
        supervisor: config.globalSettings.supervisor || '',
        // ... global settings transformation
      },
      siteDeptMapping: config.assignments.map((assignment) => {
        // ... assignment transformation with overrides
      }),
    },
  };
}
  • Timestamp Generation: Creates ISO timestamps for schedule timing
  • Nullable Field Handling: Provides sensible defaults for optional fields
  • Override Processing: Handles site-specific customizations
Schedule Creation

createBulkSchedulesFromConfig(): Main schedule creation orchestrator

async createBulkSchedulesFromConfig(
  config: BulkScheduleConfig, 
  userID: string
): Promise<{
  success: boolean;
  message: string;
  createdCount: number;
  failedCount: number;
  errors?: Array<{ siteId: string; error: string }>;
}> {
  return this.withMonitoring('create_bulk_schedules', 'bulk_schedule', async () => {
    // Validate configuration
    const validation = await this.validateBulkSchedule(config);
    if (!validation.isValid) {
      throw new Error(this.i18n._(msg`Invalid configuration: ${validation.errors?.join(', ')}`));
    }
    
    // Transform and create
    const payload = this.transformToPayload(config, undefined, userID);
    const result = await this.repository.createBulkSchedules(payload);
    
    if (!result.success) {
      throw new Error(result.message || this.i18n._(msg`Failed to create bulk schedules`));
    }
    
    return result;
  }, 'critical', {
    schedule_count: config.assignments.length,
    questionnaire_id: config.questionnaireIndexId,
    is_recurring: config.isRecurring,
  });
}
  • Pre-creation Validation: Ensures configuration validity before API calls
  • Critical Monitoring: Schedule creation has highest business impact
  • Comprehensive Error Handling: Provides detailed failure information

DraftScheduleService

Manages the complete lifecycle of schedule drafts including creation, updating, and publishing.

File: src/app/admin/bulk-schedule/_lib/draft-schedule-service.ts

Draft Lifecycle Operations

getDraftSchedules(): Paginated draft retrieval with filtering

async getDraftSchedules(dto: GetDraftSchedulesDto): Promise<DraftSchedulesResponse> {
  return this.withMonitoring('get_draft_schedules', 'draft-schedules', async () => {
    try {
      const response = await this.repository.getDraftSchedules(dto);
      
      // Additional business logic can be added here
      // For example: filtering based on user permissions, enriching data, etc.
      
      return response;
    } catch (error) {
      this.captureServiceError(error as Error, 'get_draft_schedules', 'draft-schedules', 'medium', {
        page: dto.page,
        search: dto.search,
      });
      throw new Error(this.i18n._(msg`Failed to fetch draft schedules: ${error.message}`));
    }
  }, 'medium', { page: dto.page });
}
  • Permission Hooks: Placeholder for future access control
  • Error Context: Captures search and pagination context
  • Data Enrichment: Framework for adding computed properties

searchDraftSchedules(): Convenience method for search operations

async searchDraftSchedules(searchTerm: string, page = 1): Promise<DraftSchedulesResponse> {
  return this.getDraftSchedules({
    page,
    search: searchTerm,
    sortBy: 'updatedAt',
    sortOrder: 'desc',
  });
}
Draft Modification

saveDraftSchedule(): Creates or updates draft schedules

async saveDraftSchedule(dto: SaveDraftScheduleDto): Promise<DraftSchedule> {
  return this.withMonitoring('save_draft_schedule', 'draft-schedules', async () => {
    try {
      // Validation
      if (!dto.draftName || dto.draftName.trim().length === 0) {
        throw new Error(this.i18n._(msg`Draft name is required`));
      }
      
      if (!dto.draft) {
        throw new Error(this.i18n._(msg`Draft configuration is required`));
      }
      
      const response = await this.repository.saveDraftSchedule(dto);
      return response;
    } catch (error) {
      this.captureServiceError(error as Error, 'save_draft_schedule', 'draft-schedules', 'high', {
        draft_name: dto.draftName,
        is_update: !!dto.draftId,
      });
      throw new Error(this.i18n._(msg`Failed to save draft schedule: ${error.message}`));
    }
  }, 'high', { draft_name: dto.draftName, is_update: !!dto.draftId });
}
  • Input Validation: Validates required fields before processing
  • Update Detection: Tracks whether operation is create or update
  • High Priority: Draft saving is important for user workflow
Draft Publishing

publishDraftSchedule(): Converts draft to active schedules

async publishDraftSchedule(dto: PublishDraftScheduleDto): Promise<{ scheduleIds: string[] }> {
  return this.withMonitoring('publish_draft_schedule', 'draft-schedules', async () => {
    try {
      // Pre-publish validation
      const draft = await this.repository.getDraftScheduleById({ draftId: dto.draftId });
      
      // Validate draft is ready to publish
      if (!draft.draft.schedule.questionnaireIndexID) {
        throw new Error(this.i18n._(msg`Cannot publish draft without questionnaire`));
      }
      
      const response = await this.repository.publishDraftSchedule(dto);
      
      return {
        scheduleIds: response.data.scheduleIds,
      };
    } catch (error) {
      this.captureServiceError(error as Error, 'publish_draft_schedule', 'draft-schedules', 'critical', {
        draft_id: dto.draftId,
      });
      throw new Error(this.i18n._(msg`Failed to publish draft schedule: ${error.message}`));
    }
  }, 'critical', { draft_id: dto.draftId });
}
  • Pre-publish Validation: Ensures draft completeness before publishing
  • Critical Monitoring: Publishing creates real schedules, highest impact
  • Business Rule Enforcement: Validates questionnaire requirement

Utility Functions

The module includes comprehensive utility functions for data transformation, validation, and formatting.

File: src/app/admin/bulk-schedule/_lib/bulk-schedule-utils.ts

Time and Date Utilities

getScheduleTimestamp(): Converts date and time to ISO timestamp

export function getScheduleTimestamp(date: string, timeInMinutes: number): string {
  const scheduleDate = new Date(date);
  scheduleDate.setHours(Math.floor(timeInMinutes / 60));
  scheduleDate.setMinutes(timeInMinutes % 60);
  scheduleDate.setSeconds(0);
  scheduleDate.setMilliseconds(0);
  return scheduleDate.toISOString();
}
  • Precise Time Handling: Converts minutes-based time to exact timestamps
  • Zero Padding: Ensures consistent second/millisecond values

formatTimeRange(): Human-readable time display

export function formatTimeRange(startTime?: number, endTime?: number, i18n?: I18n): string {
  if (startTime === undefined || endTime === undefined) {
    return i18n ? i18n._(msg`Not set`) : 'Not set';
  }
  
  const formatTime = (minutes: number): string => {
    const hours = Math.floor(minutes / 60);
    const mins = minutes % 60;
    const period = hours >= 12 ? 'PM' : 'AM';
    const displayHours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
    return `${displayHours}:${mins.toString().padStart(2, '0')} ${period}`;
  };
  
  return `${formatTime(startTime)} - ${formatTime(endTime)}`;
}
  • 12-Hour Format: Converts 24-hour time to user-friendly format
  • Internationalization: Supports localized “Not set” messages

Validation Utilities

validateScheduleConfig(): Comprehensive configuration validation

export function validateScheduleConfig(config: {
  questionnaireIndexId?: string;
  scheduleType?: ScheduleTypes;
  startDate?: string;
  endDate?: string;
  isAdhocOnly?: boolean;
  daysOfWeek?: number[];
  datesOfMonth?: number[];
  customDates?: string[];
}, i18n?: I18n): { isValid: boolean; errors: string[] } {
  const errors: string[] = [];
  
  if (!config.questionnaireIndexId) {
    errors.push(i18n ? i18n._(msg`Questionnaire is required`) : 'Questionnaire is required');
  }
  
  // Schedule type specific validation
  switch (config.scheduleType) {
    case ScheduleTypes.WEEKLY:
      if (!config.daysOfWeek || config.daysOfWeek.length === 0) {
        errors.push(i18n ? i18n._(msg`At least one day of week must be selected`) : 'At least one day of week must be selected');
      }
      break;
    // ... additional validation rules
  }
  
  return { isValid: errors.length === 0, errors };
}
  • Context-Aware Validation: Different rules for different schedule types
  • Localized Error Messages: All validation errors support internationalization
  • Comprehensive Coverage: Validates all required fields and business rules

Data Transformation Utilities

formatScheduleActivePeriod(): Transforms active period settings

export function formatScheduleActivePeriod(
  startDate: string,
  endDate: string,
  startTime: string,
  settings?: Partial<ScheduleAdvancedSettings>,
) {
  return {
    endActiveAt: null,
    periodLength: settings?.activePeriodValue || 1,
    periodUnit: convertPeriodUnitToSingular(settings?.activePeriodUnit),
    startActiveDate: startDate || '',
    endActiveDate: endDate || '',
    startActiveAt: startTime,
  };
}
  • API Format Conversion: Transforms UI format to API requirements
  • Default Value Handling: Provides sensible defaults for optional fields

Error Handling Strategy

Service-Level Error Handling

All services implement comprehensive error handling with the following patterns:

  1. Try-Catch Blocks: Wrap all async operations
  2. Error Classification: Categorize errors by business impact
  3. Context Capture: Record operation parameters for debugging
  4. Localized Messages: Provide user-friendly error messages
  5. Monitoring Integration: Automatic error reporting to Sentry
try {
  // Business operation
  const result = await this.repository.performOperation(params);
  return result;
} catch (error) {
  this.captureServiceError(error as Error, 'operation_name', 'feature_name', 'impact_level', {
    operation_context: params,
  });
  throw new Error(this.i18n._(msg`Localized error message: ${error.message}`));
}

Monitoring Integration

The withMonitoring() wrapper provides:

  • Performance Tracking: Measures operation duration
  • Success/Failure Rates: Tracks operation reliability
  • Business Context: Associates metrics with business impact
  • Custom Attributes: Captures operation-specific metadata

Service Integration Patterns

Dependency Injection

Services receive their dependencies through constructor injection:

export class BulkScheduleService extends BaseService {
  private readonly repository = bulkScheduleRepository;
  // Service maintains single repository instance
}

Repository Pattern

Services interact with data through repository abstractions:

// Service never directly accesses API
const response = await this.repository.getQuestionnaires();

Query Integration

Services work seamlessly with TanStack Query for caching and state management:

// Query options reference service methods
queryFn: () => bulkScheduleService.getQuestionnaires(),

The business logic layer provides a robust foundation for the bulk-schedule module, implementing complex business rules while maintaining clean separation from UI concerns and data access details.


Data Access Layer

Overview

The data access layer implements the Repository pattern to provide clean abstractions over API calls and data retrieval. All repositories extend the BaseRepository class, which provides standardized monitoring, error handling, and API client management.

BulkScheduleRepository

Primary repository for bulk schedule operations, handling questionnaires, sites, departments, users, and schedule creation.

File: src/app/admin/bulk-schedule/_lib/bulk-schedule-repository.ts

Data Retrieval Operations

Questionnaire Management

getQuestionnaires(): Retrieves available questionnaires with optimized field selection

async getQuestionnaires(): Promise<QuestionnairesApiResponseDto> {
  return this.withMonitoring('get_questionnaires', 'bulk_schedule', async () => {
    // Use minified API with specific props
    const questionnaireProps = [
      'questionnaireID',
      'organizationID',
      'title',
      'status',
      'dateCreated',
      'dateUpdated',
      'disabled',
      'type',
      'questionnaireIndexID',
      'modifiedBy',
      'tags',
      'autoAssignment',
      'questionCount',
    ];
 
    const queryParams = new URLSearchParams();
    questionnaireProps.forEach(prop => queryParams.append('props', prop));
 
    const url = `/questionnaires/questionnaireIndexes/minified?${queryParams.toString()}`;
    const response = await this.client.api.get<QuestionnairesApiResponseDto>(url);
 
    if (!response.data || response.data.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to fetch questionnaires`));
    }
 
    return response.data;
  });
}
  • Field Selection: Only requests necessary fields to minimize payload size
  • Query Parameter Building: Dynamically constructs URL parameters
  • Response Validation: Ensures API response format correctness
Site Management

getSitesForScheduling(): Retrieves sites with filtering capabilities

async getSitesForScheduling(params?: GetSitesForSchedulingDto): Promise<SitesCompactApiResponseDto> {
  return this.withMonitoring('get_sites_for_scheduling', 'bulk_schedule', async () => {
    const queryParams = new URLSearchParams();
    if (params?.search) {
      queryParams.append('search', params.search);
    }
    if (params?.departmentIds?.length) {
      queryParams.append('departmentIds', params.departmentIds.join(','));
    }
    if (params?.includeDisabled !== undefined) {
      queryParams.append('includeDisabled', params.includeDisabled.toString());
    }
    if (params?.limit) {
      queryParams.append('limit', params.limit.toString());
    }
    if (params?.offset) {
      queryParams.append('offset', params.offset.toString());
    }
 
    const queryString = queryParams.toString();
    const url = queryString ? `/sites/compact?${queryString}` : '/sites/compact';
 
    const response = await this.client.api.get<SitesCompactApiResponseDto>(url);
 
    if (!response.data || response.data.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to fetch sites`));
    }
 
    return response.data;
  }, {
    site_count: params?.limit,
    has_department_filter: Boolean(params?.departmentIds?.length),
  });
}
  • Flexible Filtering: Supports search terms, department filtering, pagination
  • Optional Parameters: Gracefully handles undefined filter parameters
  • Monitoring Context: Tracks filter usage patterns for analytics
Department and User Management

getDepartments(): Retrieves all departments for the organization

async getDepartments(): Promise<DepartmentsApiResponseDto> {
  return this.withMonitoring('get_departments', 'bulk_schedule', async () => {
    const response = await this.client.api.get<DepartmentsApiResponseDto>('/departments');
 
    if (!response.data || response.data.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to fetch departments`));
    }
 
    return response.data;
  });
}

getUsersForAuditors(): Retrieves users with role-based filtering

async getUsersForAuditors(params?: GetUsersForAuditorsDto): Promise<UsersApiResponseDto> {
  return this.withMonitoring('get_users_for_auditors', 'bulk_schedule', async () => {
    const queryParams = new URLSearchParams();
    if (params?.siteIds?.length) {
      queryParams.append('siteIds', params.siteIds.join(','));
    }
    if (params?.departmentIds?.length) {
      queryParams.append('departmentIds', params.departmentIds.join(','));
    }
    if (params?.role) {
      queryParams.append('role', params.role);
    }
    if (params?.search) {
      queryParams.append('search', params.search);
    }
 
    const queryString = queryParams.toString();
    const url = queryString ? `/users?${queryString}` : '/users';
 
    const response = await this.client.api.get<UsersApiResponseDto>(url);
 
    if (!response.data || response.data.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to fetch users`));
    }
 
    return response.data;
  }, {
    has_site_filter: Boolean(params?.siteIds?.length),
    has_department_filter: Boolean(params?.departmentIds?.length),
    role_filter: params?.role,
  });
}

Validation and Conflict Detection

checkScheduleConflicts(): Detects scheduling conflicts before creation

async checkScheduleConflicts(params: GetScheduleConflictsDto): Promise<{
  conflicts: ScheduleConflict[];
  hasConflicts: boolean;
}> {
  return this.withMonitoring('check_schedule_conflicts', 'bulk_schedule', async () => {
    const body = {
      siteIds: params.siteIds,
      departmentIds: params.departmentIds,
      startDate: params.startDate,
      endDate: params.endDate,
      questionnaireId: params.questionnaireId,
    };
 
    const response = await this.client.api.post<{
      conflicts: ScheduleConflict[];
      hasConflicts: boolean;
    }>('/schedules/check-conflicts', body);
 
    if (!response.data) {
      throw new Error(this.i18n._(msg`Failed to check schedule conflicts`));
    }
 
    return response.data;
  }, {
    site_count: params.siteIds.length,
    department_count: params.departmentIds.length,
  });
}
  • POST Request: Uses POST for complex query parameters
  • Structured Payload: Organizes parameters in request body
  • Detailed Monitoring: Tracks scope of conflict checking

Schedule Creation and Management

createBulkSchedules(): Creates multiple schedules using v2 API

async createBulkSchedules(payload: GeneralSchedulePayload): Promise<{
  success: boolean;
  message: string;
  createdCount: number;
  failedCount: number;
  errors?: Array<{ siteId: string; error: string }>;
}> {
  return this.withMonitoring('create_bulk_schedules', 'bulk_schedule', async () => {
    const response = await this.client.api.put<{
      data: unknown[];
      message: string;
      full: boolean;
    }>('/v1.0/schedules/sites/v2/bulk', payload);
 
    if (!response.data) {
      throw new Error(this.i18n._(msg`Failed to create bulk schedules`));
    }
 
    // Transform the actual API response to our expected format
    const { message } = response.data;
    const isSuccess = message === 'SUCCESS';
 
    return {
      success: isSuccess,
      message: message || this.i18n._(msg`Bulk schedules created successfully`),
      createdCount: isSuccess ? payload.data.siteDeptMapping.length : 0,
      failedCount: 0,
      errors: undefined,
    };
  }, {
    schedule_count: payload.data.siteDeptMapping.length,
    questionnaire_id: payload.data.schedule.questionnaireIndexID,
    is_recurring: payload.data.schedule.isRecurring,
  });
}
  • V2 API Endpoint: Uses the latest bulk creation API
  • PUT Method: Follows REST conventions for bulk operations
  • Response Transformation: Adapts API response to service contract
  • Comprehensive Monitoring: Captures schedule creation metrics

Template Management

getBulkScheduleTemplates(): Retrieves saved schedule templates

async getBulkScheduleTemplates(params?: GetBulkScheduleTemplatesDto): Promise<{
  templates: Array<{
    id: string;
    name: string;
    description?: string;
    config: Partial<BulkScheduleConfig>;
    createdAt: string;
    updatedAt: string;
    createdBy: { id: string; name: string };
  }>;
  total: number;
}> {
  return this.withMonitoring('get_bulk_schedule_templates', 'bulk_schedule', async () => {
    const queryParams = new URLSearchParams();
    if (params?.search) {
      queryParams.append('search', params.search);
    }
    if (params?.limit) {
      queryParams.append('limit', params.limit.toString());
    }
    if (params?.offset) {
      queryParams.append('offset', params.offset.toString());
    }
 
    const queryString = queryParams.toString();
    const url = queryString ? `/schedules/bulk/templates?${queryString}` : '/schedules/bulk/templates';
 
    const response = await this.client.api.get<{
      templates: Array<{...}>;
      total: number;
    }>(url);
 
    if (!response.data) {
      throw new Error(this.i18n._(msg`Failed to fetch bulk schedule templates`));
    }
 
    return response.data;
  }, {
    has_search: Boolean(params?.search),
  });
}

DraftScheduleRepository

Repository for managing schedule drafts with full CRUD capabilities.

File: src/app/admin/bulk-schedule/_lib/draft-schedule-repository.ts

Draft CRUD Operations

Retrieval Operations

getDraftSchedules(): Paginated draft retrieval with search and sorting

async getDraftSchedules(dto: GetDraftSchedulesDto): Promise<DraftSchedulesResponse> {
  return this.withMonitoring('get_draft_schedules', 'draft-schedules', async () => {
    const params = new URLSearchParams({
      page: dto.page.toString(),
      ...(dto.limit && { limit: dto.limit.toString() }),
      ...(dto.search && { search: dto.search }),
      ...(dto.sortBy && { sortBy: dto.sortBy }),
      ...(dto.sortOrder && { sortOrder: dto.sortOrder }),
    });
 
    const response = await this.client.api.get<GetDraftSchedulesResponseDto>(
      `${this.BASE_URL}?${params.toString()}`,
    );
 
    if (response.data?.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to fetch draft schedules`));
    }
 
    // Transform the response to match our domain entities
    const transformedData: DraftSchedulesResponse = {
      ...response.data,
      data: {
        ...response.data.data,
        data: response.data.data.data.map(item => ({
          ...item,
          draft: item.draft as DraftScheduleData,
        })),
      },
    };
 
    return transformedData;
  }, { page: dto.page, search: dto.search });
}
  • URL Construction: Dynamically builds query string from parameters
  • Type Transformation: Converts API response to domain entity format
  • Base URL: Uses centralized URL constant for consistency

getDraftScheduleById(): Retrieves single draft by ID

async getDraftScheduleById(dto: GetDraftScheduleByIdDto): Promise<DraftSchedule> {
  return this.withMonitoring('get_draft_schedule_by_id', 'draft-schedules', async () => {
    const params = new URLSearchParams({
      page: '1',
      draftId: dto.draftId,
    });
 
    const response = await this.client.api.get<GetDraftScheduleByIdResponseDto>(
      `${this.BASE_URL}?${params.toString()}`,
    );
 
    if (response.data?.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to fetch draft schedule`));
    }
 
    // Extract the first item from the data array
    const draftData = response.data.data.data[0];
    if (!draftData) {
      throw new Error(this.i18n._(msg`Draft schedule not found`));
    }
 
    return draftData as DraftSchedule;
  }, { draft_id: dto.draftId });
}
  • Array Extraction: Handles API response that returns arrays
  • Not Found Handling: Provides specific error for missing drafts
Modification Operations

saveDraftSchedule(): Creates new drafts or updates existing ones

async saveDraftSchedule(dto: SaveDraftScheduleDto): Promise<DraftSchedule> {
  return this.withMonitoring('save_draft_schedule', 'draft-schedules', async () => {
    const response = await this.client.api.post<SaveDraftScheduleResponseDto>(
      this.BASE_URL,
      dto,
    );
 
    if (response.data?.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to save draft schedule`));
    }
 
    return response.data.data as DraftSchedule;
  }, { draft_name: dto.draftName, is_update: !!dto.draftId });
}

updateDraftSchedule(): Updates existing draft properties

async updateDraftSchedule(dto: UpdateDraftScheduleDto): Promise<UpdateDraftScheduleResponseDto> {
  return this.withMonitoring('update_draft_schedule', 'draft-schedules', async () => {
    const { draftId, ...updateData } = dto;
 
    const response = await this.client.api.put<UpdateDraftScheduleResponseDto>(
      `${this.BASE_URL}/${draftId}`,
      updateData,
    );
 
    if (response.data?.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to update draft schedule`));
    }
 
    return response.data;
  }, { draft_id: dto.draftId });
}
Delete and Publish Operations

deleteDraftSchedule(): Removes draft from system

async deleteDraftSchedule(dto: DeleteDraftScheduleDto): Promise<DeleteDraftScheduleResponseDto> {
  return this.withMonitoring('delete_draft_schedule', 'draft-schedules', async () => {
    const response = await this.client.api.delete<DeleteDraftScheduleResponseDto>(
      `${this.BASE_URL}/${dto.draftId}`,
    );
 
    if (response.data?.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to delete draft schedule`));
    }
 
    return response.data;
  }, { draft_id: dto.draftId });
}

publishDraftSchedule(): Converts draft to active schedules

async publishDraftSchedule(dto: PublishDraftScheduleDto): Promise<PublishDraftScheduleResponseDto> {
  return this.withMonitoring('publish_draft_schedule', 'draft-schedules', async () => {
    const response = await this.client.api.post<PublishDraftScheduleResponseDto>(
      `${this.BASE_URL}/${dto.draftId}/publish`,
      {},
    );
 
    if (response.data?.message !== 'SUCCESS') {
      throw new Error(this.i18n._(msg`Failed to publish draft schedule`));
    }
 
    return response.data;
  }, { draft_id: dto.draftId });
}

Query Management with TanStack Query

The module uses TanStack Query for intelligent caching and state management of API data.

File: src/app/admin/bulk-schedule/_lib/bulk-schedule-queries.ts

Query Key Factory

Centralized query key management for consistency and cache invalidation:

export const bulkScheduleQueryKeys = {
  all: ['bulk-schedule'] as const,
  questionnaires: () => ['bulk-schedule', 'questionnaires'] as const,
  sites: (params?: GetSitesForSchedulingDto) => ['bulk-schedule', 'sites', params] as const,
  users: (params?: GetUsersForAuditorsDto) => ['bulk-schedule', 'users', params] as const,
  departments: () => ['bulk-schedule', 'departments'] as const,
  conflicts: (params: GetScheduleConflictsDto) => ['bulk-schedule', 'conflicts', params] as const,
  preview: (config: Partial<BulkScheduleConfig>) => ['bulk-schedule', 'preview', config] as const,
  templates: (search?: string) => ['bulk-schedule', 'templates', search] as const,
};

Caching Strategies

Different data types use appropriate caching strategies based on change frequency:

Long-lived Data (questionnaires, departments):

questionnaires: () => queryOptions({
  queryKey: bulkScheduleQueryKeys.questionnaires(),
  queryFn: () => bulkScheduleService.getQuestionnaires(),
  staleTime: 10 * 60 * 1000, // 10 minutes
  gcTime: 15 * 60 * 1000, // 15 minutes
  retry: 2,
}),

Medium-lived Data (sites, users):

sites: (params?: GetSitesForSchedulingDto) => queryOptions({
  queryKey: bulkScheduleQueryKeys.sites(params),
  queryFn: () => bulkScheduleService.getSitesForScheduling(params),
  staleTime: 5 * 60 * 1000, // 5 minutes
  gcTime: 10 * 60 * 1000, // 10 minutes
  retry: 2,
}),

Short-lived Data (conflicts, previews):

conflicts: (params: GetScheduleConflictsDto) => queryOptions({
  queryKey: bulkScheduleQueryKeys.conflicts(params),
  queryFn: () => bulkScheduleService.checkScheduleConflicts(params),
  staleTime: 0, // Always fresh
  gcTime: 0, // No cache
  retry: 1,
}),

Mutation Options

Standardized mutation configuration with optimistic updates:

export const bulkScheduleMutationOptions = {
  create: () => mutationOptions({
    mutationKey: ['bulk-schedule', 'create'],
    mutationFn: (payload: GeneralSchedulePayload) =>
      bulkScheduleService.createBulkSchedules(payload),
  }),
 
  validate: () => mutationOptions({
    mutationKey: ['bulk-schedule', 'validate'],
    mutationFn: (config: BulkScheduleConfig) =>
      bulkScheduleService.validateBulkSchedule(config),
  }),
};

Draft Query Management

File: src/app/admin/bulk-schedule/_lib/draft-schedule-queries.ts

Draft-Specific Query Keys

export const draftScheduleKeys = {
  all: ['draft-schedules'] as const,
  lists: () => [...draftScheduleKeys.all, 'list'] as const,
  list: (filters: GetDraftSchedulesDto) => [...draftScheduleKeys.lists(), filters] as const,
  details: () => [...draftScheduleKeys.all, 'detail'] as const,
  detail: (id: string) => [...draftScheduleKeys.details(), id] as const,
};

Draft Query Options

export const draftScheduleQueryOptions = {
  list: (filters: GetDraftSchedulesDto) =>
    queryOptions({
      queryKey: draftScheduleKeys.list(filters),
      queryFn: () => draftScheduleService.getDraftSchedules(filters),
      staleTime: 30 * 1000, // 30 seconds
      gcTime: 5 * 60 * 1000, // 5 minutes
    }),
 
  detail: (draftId: string) =>
    queryOptions({
      queryKey: draftScheduleKeys.detail(draftId),
      queryFn: () => draftScheduleService.getDraftScheduleById({ draftId }),
      staleTime: 60 * 1000, // 1 minute
      gcTime: 10 * 60 * 1000, // 10 minutes
    }),
};

API Endpoint Summary

The data access layer interacts with multiple API endpoints across different services:

EndpointMethodPurposeRepository Method
/questionnaires/questionnaireIndexes/minifiedGETGet available questionnairesgetQuestionnaires()
/sites/compactGETGet sites for schedulinggetSitesForScheduling()
/departmentsGETGet all departmentsgetDepartments()
/usersGETGet users for auditor selectiongetUsersForAuditors()
/schedules/check-conflictsPOSTCheck for schedule conflictscheckScheduleConflicts()
/schedules/bulk/previewPOSTGenerate schedule previewgeneratePreview()
/v1.0/schedules/sites/v2/bulkPUTCreate bulk schedulescreateBulkSchedules()
/schedules/bulk/validatePOSTValidate bulk configurationvalidateBulkSchedule()
/schedules/bulk/templatesGETGet schedule templatesgetBulkScheduleTemplates()
/schedules/bulk/templatesPOSTSave schedule templatesaveBulkScheduleTemplate()
/schedules/bulk/templates/{id}DELETEDelete schedule templatedeleteBulkScheduleTemplate()
/v1.0/schedules/sites/draftGETGet draft schedulesgetDraftSchedules()
/v1.0/schedules/sites/draftPOSTSave draft schedulesaveDraftSchedule()
/v1.0/schedules/sites/draft/{id}PUTUpdate draft scheduleupdateDraftSchedule()
/v1.0/schedules/sites/draft/{id}DELETEDelete draft scheduledeleteDraftSchedule()
/v1.0/schedules/sites/draft/{id}/publishPOSTPublish draft schedulepublishDraftSchedule()

Repository Pattern Benefits

The repository pattern provides several architectural advantages:

  1. Abstraction: Services work with clean interfaces rather than HTTP details
  2. Testability: Easy to mock repositories for unit testing
  3. Consistency: Standardized error handling and monitoring across all data access
  4. Caching: Centralized caching strategy through TanStack Query integration
  5. Type Safety: Full TypeScript typing from request to response
  6. Monitoring: Built-in performance and error tracking
  7. Internationalization: Consistent error message localization

The data access layer provides a solid foundation for the bulk-schedule module, abstracting away API complexities while providing robust error handling, monitoring, and caching capabilities.


User Interface Components

Overview

The user interface layer implements a comprehensive set of React components that provide both the bulk schedule creation wizard and draft management functionality. The components follow modern React patterns with TypeScript, utilize Jotai for state management, and are built with accessibility and responsive design in mind.

Wizard Architecture

The bulk schedule creation process is implemented as a multi-step wizard with the following component hierarchy:

graph TD
    A[BulkScheduleWizard] --> B[WizardHeader]
    A --> C[WizardStepIndicator]
    A --> D[WizardNavigation]
    A --> E[ConfigureSchedule]
    A --> F[GlobalAdvancedSettings]
    A --> G[SiteDepartmentAssignment]
    A --> H[SchedulePreview]
    
    E --> E1[QuestionnaireSelection]
    E --> E2[ScheduleTypeSelector]
    E --> E3[DateRangePicker]
    E --> E4[ScheduleModeSelector]
    
    F --> F1[AuditorSelection]
    F --> F2[TimeSettings]
    F --> F3[FeatureToggles]
    F --> F4[NotificationSettings]
    
    G --> G1[SiteSelection]
    G2[AssignmentList]
    G --> G3[AssignmentAdvancedSettings]
    
    H --> H1[SchedulePreviewTable]
    H --> H2[ConflictDisplay]
    H --> H3[ValidationSummary]

Core Wizard Components

BulkScheduleWizard

The main orchestrator component that manages the entire wizard flow and state coordination.

File: src/app/admin/bulk-schedule/create/_components/bulk-schedule-wizard.tsx

Key Responsibilities:

  • State Management: Orchestrates all Jotai atoms and wizard state
  • Data Loading: Background fetching of sites, departments, users, questionnaires
  • Draft Management: Loading existing drafts and save/update functionality
  • Navigation Control: Step transitions and validation
  • Error Handling: Comprehensive error states and recovery

Core Implementation Patterns:

export function BulkScheduleWizard({ draftId }: BulkScheduleWizardProps) {
  const { i18n } = useLingui();
  const { navigateAfterCreation } = useBulkScheduleNavigation();
  
  // State management
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [showSuccessModal, setShowSuccessModal] = useState(false);
  const [createdCount, setCreatedCount] = useState(0);
  
  // Query client for cache management
  const queryClient = useQueryClient();
  
  // Atom setters for background data loading
  const setAvailableUsers = useSetAtom(availableUsersAtom);
  const setAvailableSites = useSetAtom(availableSitesAtom);
  const setAvailableDepartments = useSetAtom(availableDepartmentsAtom);
  
  // Background data fetching
  const { data: usersData } = useQuery(bulkScheduleQueryOptions.users());
  const { data: sitesData, isLoading: sitesLoading } = useQuery(bulkScheduleQueryOptions.sites());
  const { data: departmentsData, isLoading: departmentsLoading } = useQuery(bulkScheduleQueryOptions.departments());
  const { data: questionnairesData } = useQuery(bulkScheduleQueryOptions.questionnaires());
  
  // Wizard state management
  const {
    state,
    updateConfig,
    isStepValid,
    handleNext,
    handlePrevious,
    handleStepClick,
  } = useWizardState({
    steps: translatedWizardSteps,
    validateStep,
  });
}

Draft Loading Logic:

// Load draft if draftId is provided
const { data: draftData, isLoading: isDraftLoading } = useQuery({
  ...draftScheduleQueryOptions.detail(draftId || ''),
  enabled: !!draftId && !draftLoaded && !isSubmitting,
});
 
// Transform and load draft data
useEffect(() => {
  if (draftData && draftId && !draftLoaded) {
    const transformedConfig = transformDraftToWizardConfig(draftData);
    
    // Enrich with current data
    if (transformedConfig.assignments && sitesData && departmentsData) {
      // Data enrichment logic...
    }
    
    updateConfig(transformedConfig);
    setDraftLoaded(true);
    toast.success(i18n._(msg`Draft loaded successfully`));
  }
}, [draftData, draftId, draftLoaded, updateConfig, sitesData, departmentsData]);

WizardHeader

Displays current step progress and provides contextual information.

File: src/app/admin/bulk-schedule/create/_components/wizard/wizard-header.tsx

Features:

  • Step progress indicator
  • Contextual help text
  • Responsive design for mobile/desktop
  • Internationalization support

WizardStepIndicator

Visual step navigation with clickable steps and status indicators.

File: src/app/admin/bulk-schedule/create/_components/wizard/wizard-step-indicator.tsx

export function WizardStepIndicator({
  steps,
  currentStep,
  stepStatus,
  onStepClick,
}: WizardStepIndicatorProps) {
  return (
    <div className="flex items-center justify-between w-full">
      {steps.map((step, index) => {
        const status = stepStatus[step.id] || 'pending';
        const isClickable = index <= currentStep || status === 'completed';
        
        return (
          <button
            key={step.id}
            onClick={() => isClickable && onStepClick(index)}
            disabled={!isClickable}
            className={cn(
              'flex items-center space-x-2 px-4 py-2 rounded-lg transition-colors',
              status === 'current' && 'bg-primary text-primary-foreground',
              status === 'completed' && 'bg-green-100 text-green-800',
              status === 'pending' && 'bg-gray-100 text-gray-500',
              isClickable && 'hover:bg-opacity-80 cursor-pointer',
            )}
          >
            <StepIcon status={status} />
            <span>{step.label}</span>
          </button>
        );
      })}
    </div>
  );
}

Step Components

ConfigureSchedule (Step 1)

The first step handles questionnaire selection and basic schedule configuration.

File: src/app/admin/bulk-schedule/create/_components/configure-schedule.tsx

Sub-components:

  • QuestionnaireSelection: Searchable dropdown with questionnaire details
  • ScheduleTypeSelector: Radio buttons for daily/weekly/monthly/custom
  • DateRangePicker: Date range selection with validation
  • ScheduleModeSelector: Toggle between scheduled and ad-hoc only modes
  • RecurrenceSettings: Pattern-specific configuration (days of week, dates of month, etc.)

Validation Logic:

const canProceedFromConfigureStepAtom = atom((get) => {
  const questionnaire = get(selectedQuestionnaireAtom);
  const config = get(basicScheduleConfigAtom);
  const settings = get(globalAdvancedSettingsAtom);
 
  // Ad hoc mode only needs questionnaire
  if (config.isAdhocOnly) {
    return Boolean(questionnaire?.questionnaireIndexId);
  }
 
  // Base required fields for scheduled modes
  const hasBaseFields = Boolean(
    questionnaire?.questionnaireIndexId
    && config.scheduleType
    && config.startDate
    && settings.startTime !== undefined
    && settings.endTime !== undefined,
  );
 
  if (!hasBaseFields) return false;
 
  // Pattern-specific validation
  switch (config.scheduleType) {
    case ScheduleTypes.WEEKLY:
      return (config.daysOfWeek?.length ?? 0) > 0;
    case ScheduleTypes.MONTHLY:
      return (config.datesOfMonth?.length ?? 0) > 0;
    case ScheduleTypes.CUSTOM:
      return (config.customDates?.length ?? 0) > 0;
    default:
      return true;
  }
});

GlobalAdvancedSettings (Step 2)

Optional step for configuring default auditors, time settings, and features.

File: src/app/admin/bulk-schedule/create/_components/global-advanced-settings.tsx

Feature Categories:

Personnel Management:

  • Auditor assignment (single or multiple)
  • Supervisor selection
  • Issue owner configuration

Time Configuration:

  • Start and end times
  • Time zone handling
  • Deadline settings
  • Reminder configuration

Compliance Features:

  • Check-in/checkout enforcement
  • Signature requirements
  • Photo signature settings

Notification Settings:

  • Email target configuration
  • Reminder timing and frequency

SiteDepartmentAssignment (Step 3)

Complex component for managing site-department combinations with override capabilities.

File: src/app/admin/bulk-schedule/create/_components/site-department-assignment.tsx

Key Features:

Bulk Selection:

  • Site filtering and search
  • Department filtering
  • Bulk assignment creation

Assignment Management:

  • Individual assignment removal
  • Expandable override configuration
  • Visual indicators for customized assignments

Override System:

// Per-assignment overrides
const AssignmentAdvancedSettings = ({ assignment, globalSettings, onChange }) => {
  const [overrides, setOverrides] = useState(assignment.overrides || {});
  
  const handleOverrideChange = (field: keyof ScheduleAdvancedSettings, value: any) => {
    const newOverrides = { ...overrides, [field]: value };
    setOverrides(newOverrides);
    onChange(assignment.id, newOverrides);
  };
  
  return (
    <div className="space-y-4 p-4 border rounded-lg">
      {/* Time override controls */}
      <TimeSettingsOverride 
        globalStartTime={globalSettings.startTime}
        globalEndTime={globalSettings.endTime}
        overrideStartTime={overrides.startTime}
        overrideEndTime={overrides.endTime}
        onChange={handleOverrideChange}
      />
      
      {/* Auditor override controls */}
      <AuditorOverride
        globalAuditors={globalSettings.auditors}
        overrideAuditors={overrides.auditors}
        onChange={handleOverrideChange}
      />
    </div>
  );
};

SchedulePreview (Step 4)

Final step showing a comprehensive preview of all schedules to be created.

File: src/app/admin/bulk-schedule/create/_components/schedule-preview.tsx

Preview Components:

Summary Statistics:

  • Total schedules to be created
  • Assignment breakdown by site/department
  • Conflict warnings and resolution suggestions

Detailed Preview Table:

  • Site and department information
  • Schedule timing and recurrence
  • Assigned auditors and supervisors
  • Override indicators and details

Conflict Detection Display:

const ConflictDisplay = ({ conflicts }: { conflicts: ScheduleConflict[] }) => {
  const errorConflicts = conflicts.filter(c => c.severity === 'error');
  const warningConflicts = conflicts.filter(c => c.severity === 'warning');
  
  return (
    <div className="space-y-4">
      {errorConflicts.length > 0 && (
        <Alert variant="destructive">
          <AlertTriangle className="h-4 w-4" />
          <AlertTitle>Schedule Conflicts Detected</AlertTitle>
          <AlertDescription>
            The following conflicts must be resolved before creating schedules:
            {errorConflicts.map((conflict, index) => (
              <div key={index} className="mt-2 p-2 bg-red-50 rounded">
                <p className="font-medium">{conflict.siteName} - {conflict.departmentName}</p>
                <p className="text-sm">{conflict.message}</p>
              </div>
            ))}
          </AlertDescription>
        </Alert>
      )}
      
      {warningConflicts.length > 0 && (
        <Alert variant="warning">
          <AlertTriangle className="h-4 w-4" />
          <AlertTitle>Potential Issues</AlertTitle>
          <AlertDescription>
            Please review the following warnings:
            {warningConflicts.map((conflict, index) => (
              <div key={index} className="mt-2 p-2 bg-yellow-50 rounded">
                <p className="font-medium">{conflict.siteName} - {conflict.departmentName}</p>
                <p className="text-sm">{conflict.message}</p>
              </div>
            ))}
          </AlertDescription>
        </Alert>
      )}
    </div>
  );
};

Draft Management Components

DraftScheduleTable

Desktop-optimized table view for managing draft schedules.

File: src/app/admin/bulk-schedule/drafts/_components/draft-schedule-table.tsx

Features:

  • Sortable columns
  • Pagination integration
  • Action buttons (edit, delete, publish)
  • Questionnaire name resolution
  • Schedule type and timing display
const DraftScheduleRow = memo(({ draft }: { draft: DraftSchedule }) => {
  const questionnaireId = draft.draft.schedule.questionnaireIndexID;
  const questionnaireName = useQuestionnaireName(questionnaireId);
  
  const getScheduleModeLabel = () => {
    if (!draft.draft.schedule.isRecurring) {
      return <Trans>One-time</Trans>;
    }
    const type = draft.draft.schedule.type;
    return (
      <Trans>
        Recurring - {DraftScheduleBusinessRules.getScheduleTypeLabel(type)}
      </Trans>
    );
  };
  
  return (
    <TableRow>
      <TableCell className="font-medium">{draft.draftName}</TableCell>
      <TableCell>{questionnaireName || <Trans>Loading...</Trans>}</TableCell>
      <TableCell>
        <Badge variant={draft.draft.schedule.isRecurring ? 'default' : 'secondary'}>
          {getScheduleModeLabel()}
        </Badge>
      </TableCell>
      <TableCell>{formatDateTime(draft.draft.schedule.firstScheduleStart)}</TableCell>
      <TableCell>{formatDateTime(draft.draft.schedule.firstScheduleEnd)}</TableCell>
      <TableCell>
        <DraftTableActions draft={draft} />
      </TableCell>
    </TableRow>
  );
});

DraftScheduleCard

Mobile-optimized card view for draft management.

File: src/app/admin/bulk-schedule/drafts/_components/draft-schedule-card.tsx

Mobile-First Design:

  • Compact information display
  • Touch-friendly action buttons
  • Collapsible details
  • Responsive layout adaptation

SearchFilters

Comprehensive filtering interface for draft discovery.

File: src/app/admin/bulk-schedule/drafts/_components/search-filters.tsx

Filter Capabilities:

  • Text search across draft names
  • Date range filtering
  • Schedule type filtering
  • Created by user filtering
  • Sort order controls

State Management with Jotai

The UI layer uses Jotai atoms for granular, reactive state management:

File: src/app/admin/bulk-schedule/_lib/bulk-schedule-atoms.ts

Core State Atoms

Wizard Navigation:

export const currentStepAtom = atom(0);
export const stepStatusAtom = atom<Record<string, WizardStepStatus>>({
  questionnaire: 'current',
  basic: 'pending',
  advanced: 'pending',
  assignment: 'pending',
  preview: 'pending',
});

Configuration State:

export const selectedQuestionnaireAtom = atom<{
  questionnaireIndexId: string;
  questionnaireName: string;
} | null>(null);
 
export const basicScheduleConfigAtom = atom<{
  scheduleType?: ScheduleTypes;
  isRecurring?: boolean;
  startDate?: string;
  endDate?: string;
  // ... additional fields
}>({});
 
export const globalAdvancedSettingsAtom = atom<ScheduleAdvancedSettings>({});
export const siteDepartmentAssignmentsAtom = atom<SiteDepartmentAssignment[]>([]);

Derived Atoms

Complete Configuration:

export const bulkScheduleConfigAtom = atom<Partial<BulkScheduleConfig>>((get) => {
  const questionnaire = get(selectedQuestionnaireAtom);
  const basicConfig = get(basicScheduleConfigAtom);
  const globalSettings = get(globalAdvancedSettingsAtom);
  const assignments = get(siteDepartmentAssignmentsAtom);
 
  return {
    ...(questionnaire && {
      questionnaireIndexId: questionnaire.questionnaireIndexId,
      questionnaireName: questionnaire.questionnaireName,
    }),
    ...basicConfig,
    globalSettings,
    assignments,
  };
});

Step Validation:

export const canProceedToStep2Atom = atom((get) => {
  const questionnaire = get(selectedQuestionnaireAtom);
  const config = get(basicScheduleConfigAtom);
  const settings = get(globalAdvancedSettingsAtom);
  
  // Validation logic specific to step 1
  return Boolean(
    questionnaire?.questionnaireIndexId
    && config.scheduleType
    && config.startDate
    && settings.startTime !== undefined
  );
});

Custom Hooks

The module provides specialized hooks for complex UI interactions:

File: src/app/admin/bulk-schedule/_lib/bulk-schedule-hooks.ts

useWizardNavigation

Manages step transitions and validation:

export function useWizardNavigation(steps: Array<{ id: string; label: string }>) {
  const [currentStep, setCurrentStep] = useAtom(currentStepAtom);
  const [stepStatus, setStepStatus] = useAtom(stepStatusAtom);
  
  const canProceed = useCallback((stepIndex: number): boolean => {
    switch (stepIndex) {
      case 0: return canProceedToStep2;
      case 1: return canProceedToStep3;
      case 2: return canProceedToStep4;
      case 3: return canProceedToStep5;
      default: return false;
    }
  }, [canProceedToStep2, canProceedToStep3, canProceedToStep4, canProceedToStep5]);
 
  const goToStep = useCallback((targetStep: number) => {
    if (targetStep < 0 || targetStep >= steps.length) return;
    
    setCurrentStep(targetStep);
    setStepStatus(prev => ({
      ...prev,
      [steps[targetStep].id]: 'current',
    }));
  }, [currentStep, setCurrentStep, setStepStatus, steps]);
 
  return {
    currentStep,
    stepStatus,
    canProceed,
    goToStep,
    goToNext: () => goToStep(currentStep + 1),
    goToPrevious: () => goToStep(currentStep - 1),
  };
}

useSiteDepartmentAssignments

Manages complex assignment operations:

export function useSiteDepartmentAssignments() {
  const [assignments, setAssignments] = useAtom(siteDepartmentAssignmentsAtom);
  const [overrides, setOverrides] = useAtom(assignmentOverridesAtom);
  const setIsDirty = useSetAtom(isDirtyAtom);
 
  const addAssignment = useCallback((assignment: SiteDepartmentAssignment) => {
    setAssignments(prev => [...prev, assignment]);
    setIsDirty(true);
  }, [setAssignments, setIsDirty]);
 
  const updateOverrides = useCallback((id: string, overrides: Partial<ScheduleAdvancedSettings> | null) => {
    if (overrides && Object.keys(overrides).length > 0) {
      setOverrides(prev => ({ ...prev, [id]: overrides }));
      setAssignments(prev =>
        prev.map(a => a.id === id ? { ...a, hasOverrides: true } : a),
      );
    } else {
      setOverrides(prev => {
        const newOverrides = { ...prev };
        delete newOverrides[id];
        return newOverrides;
      });
      setAssignments(prev =>
        prev.map(a => a.id === id ? { ...a, hasOverrides: false, overrides: undefined } : a),
      );
    }
    setIsDirty(true);
  }, [setOverrides, setAssignments, setIsDirty]);
 
  return {
    assignments,
    addAssignment,
    updateOverrides,
    // ... additional methods
  };
}

Responsive Design Implementation

The components implement mobile-first responsive design:

Adaptive Layout Patterns

Desktop Layout:

  • Multi-column forms
  • Table-based data display
  • Side-by-side navigation
  • Expanded information density

Mobile Layout:

  • Single-column forms
  • Card-based data display
  • Bottom navigation
  • Touch-optimized interactions
// Responsive detection hook
export const getIsMobile = () => {
  if (typeof window === 'undefined') return false;
  return window.innerWidth < 768;
};
 
// Conditional rendering based on screen size
{isMobile ? (
  <DraftScheduleCard key={draft.draftId} draft={draft} />
) : (
  <DraftScheduleTable drafts={drafts} isLoading={isLoading} />
)}

Error Handling and Loading States

Comprehensive Error Boundaries

Service Error Display:

if (error) {
  return (
    <div className="space-y-6 px-8">
      <div className="rounded-lg border border-destructive/50 p-4">
        <p className="text-sm text-destructive">
          <Trans>Failed to load draft schedules. Please try again.</Trans>
        </p>
      </div>
    </div>
  );
}

Loading States:

if (isLoading) {
  return (
    <div className="flex min-h-screen items-center justify-center">
      <div className="flex flex-col items-center gap-4">
        <Loader2 className="h-8 w-8 animate-spin text-primary" />
        <p className="text-sm text-muted-foreground">
          <Trans>Loading draft schedule...</Trans>
        </p>
      </div>
    </div>
  );
}

Skeleton Loading

Table Skeletons:

export const DraftTableSkeleton = () => {
  return Array.from({ length: 5 }).map((_, index) => (
    <TableRow key={index}>
      <TableCell><Skeleton className="h-4 w-[150px]" /></TableCell>
      <TableCell><Skeleton className="h-4 w-[200px]" /></TableCell>
      <TableCell><Skeleton className="h-6 w-[100px]" /></TableCell>
      <TableCell><Skeleton className="h-4 w-[120px]" /></TableCell>
      <TableCell><Skeleton className="h-4 w-[120px]" /></TableCell>
      <TableCell><Skeleton className="h-8 w-[80px]" /></TableCell>
    </TableRow>
  ));
};

Accessibility Implementation

WCAG 2.1 AA Compliance

Keyboard Navigation:

  • All interactive elements are keyboard accessible
  • Proper tab order throughout wizard steps
  • Arrow key navigation for step indicators

Screen Reader Support:

  • Semantic HTML structure
  • ARIA labels and descriptions
  • Live regions for dynamic content updates

Visual Accessibility:

  • High contrast color schemes
  • Focus indicators on all interactive elements
  • Scalable text and UI components
// Example ARIA implementation
<button
  onClick={() => onStepClick(index)}
  aria-label={`${step.label}${step.isOptional ? ' (Optional)' : ''}`}
  aria-current={status === 'current' ? 'step' : undefined}
  aria-describedby={`step-${index}-description`}
  className={stepButtonClasses}
>
  <StepIcon status={status} />
  <span>{step.label}</span>
  {step.isOptional && (
    <span className="text-xs opacity-70">
      <Trans>(Optional)</Trans>
    </span>
  )}
</button>

The user interface layer provides a comprehensive, accessible, and responsive experience for bulk schedule management, implementing modern React patterns while maintaining excellent user experience across all device types and accessibility requirements.


Features Deep Dive

Overview

The bulk-schedule module implements sophisticated features that handle complex scheduling scenarios, validation, and user workflows. This section explores the detailed implementation of key features including the wizard workflow, draft management, conflict detection, and override system.

Bulk Schedule Creation Wizard

Multi-Step Workflow Implementation

The wizard follows a carefully designed flow that guides users through complex schedule creation:

flowchart TD
    A[Start Wizard] --> B{Draft ID?}
    B -->|Yes| C[Load Draft Data]
    B -->|No| D[Initialize Empty State]
    
    C --> E[Step 1: Configure Schedule]
    D --> E
    
    E --> F{Questionnaire Selected?}
    F -->|No| G[Show Validation Error]
    F -->|Yes| H{Schedule Type Valid?}
    
    H -->|No| G
    H -->|Yes| I[Step 2: Advanced Settings - Optional]
    
    I --> J[Step 3: Site Assignment]
    J --> K{Assignments Created?}
    K -->|No| L[Show Assignment Error]
    K -->|Yes| M[Generate Preview]
    
    M --> N[Step 4: Review & Create]
    N --> O{Conflicts Detected?}
    O -->|Yes| P[Show Conflict Warnings]
    O -->|No| Q[Ready to Create]
    
    P --> R{User Confirms?}
    Q --> R
    R -->|No| S[Return to Previous Step]
    R -->|Yes| T[Create Schedules]
    
    T --> U[Success Modal]
    U --> V[Navigate to Schedules or Create New]

Step 1: Schedule Configuration

Core Configuration Logic:

// Time-based validation with timezone awareness
const validateScheduleTiming = (config: Partial<BulkScheduleConfig>): ValidationResult => {
  const errors: string[] = [];
  
  // Start time validation
  if (config.globalSettings?.startTime === undefined) {
    errors.push('Start time is required');
  }
  
  // End time validation with next-day handling
  if (config.globalSettings?.endTime === undefined) {
    errors.push('End time is required');
  } else if (config.globalSettings.startTime !== undefined) {
    const startMinutes = config.globalSettings.startTime;
    const endMinutes = config.globalSettings.endTime;
    
    // Handle overnight schedules (end time next day)
    if (endMinutes <= startMinutes) {
      config.globalSettings.isEndTimeNextDay = true;
    }
  }
  
  return { isValid: errors.length === 0, errors };
};

Pattern-Specific Validation:

const validateRecurrencePattern = (config: Partial<BulkScheduleConfig>): ValidationResult => {
  switch (config.scheduleType) {
    case ScheduleTypes.WEEKLY:
      // Weekly schedules require at least one day
      if (!config.daysOfWeek || config.daysOfWeek.length === 0) {
        return { isValid: false, errors: ['At least one day of the week must be selected'] };
      }
      
      // Validate day numbers (0-6, Sunday-Saturday)
      const invalidDays = config.daysOfWeek.filter(day => day < 0 || day > 6);
      if (invalidDays.length > 0) {
        return { isValid: false, errors: ['Invalid day selection'] };
      }
      break;
      
    case ScheduleTypes.MONTHLY:
      // Monthly schedules require dates
      if (!config.datesOfMonth || config.datesOfMonth.length === 0) {
        return { isValid: false, errors: ['At least one date must be selected'] };
      }
      
      // Validate date numbers (1-31)
      const invalidDates = config.datesOfMonth.filter(date => date < 1 || date > 31);
      if (invalidDates.length > 0) {
        return { isValid: false, errors: ['Invalid date selection'] };
      }
      break;
      
    case ScheduleTypes.CUSTOM:
      // Custom schedules require specific dates
      if (!config.customDates || config.customDates.length === 0) {
        return { isValid: false, errors: ['At least one custom date must be selected'] };
      }
      
      // Validate date format and range
      const now = new Date();
      const invalidDates = config.customDates.filter(dateStr => {
        const date = new Date(dateStr);
        return isNaN(date.getTime()) || date < now;
      });
      
      if (invalidDates.length > 0) {
        return { isValid: false, errors: ['One or more custom dates are invalid or in the past'] };
      }
      break;
  }
  
  return { isValid: true, errors: [] };
};

Step 2: Advanced Settings

Feature Toggle Implementation:

const AdvancedFeatureToggles = ({ settings, onChange }: AdvancedSettingsProps) => {
  const [localSettings, setLocalSettings] = useState(settings);
  
  // Check-in/out enforcement with business logic
  const handleEnforceCheckInChange = (enforceCheckIn: boolean) => {
    const updatedSettings = {
      ...localSettings,
      enforceCheckIn,
      // Auto-enable check-out when check-in is enabled
      enforceCheckOut: enforceCheckIn ? true : localSettings.enforceCheckOut,
    };
    
    setLocalSettings(updatedSettings);
    onChange(updatedSettings);
  };
  
  // Signature requirements with validation
  const handleSignatureChange = (signatures: number) => {
    const updatedSettings = {
      ...localSettings,
      signatures: Math.max(0, Math.min(3, signatures)), // Limit to 0-3 signatures
      // Reset selfie signatures if no signatures required
      selfieSignatures: signatures > 0 ? localSettings.selfieSignatures : false,
    };
    
    setLocalSettings(updatedSettings);
    onChange(updatedSettings);
  };
  
  return (
    <div className="space-y-6">
      {/* Check-in/out enforcement */}
      <div className="space-y-4">
        <h4 className="font-medium">Location Verification</h4>
        <div className="space-y-3">
          <ToggleSwitch
            label="Enforce Check-in"
            checked={localSettings.enforceCheckIn}
            onChange={handleEnforceCheckInChange}
            description="Require auditors to check-in at the location"
          />
          
          <ToggleSwitch
            label="Enforce Check-out"
            checked={localSettings.enforceCheckOut}
            onChange={(enforceCheckOut) => onChange({ ...localSettings, enforceCheckOut })}
            disabled={!localSettings.enforceCheckIn}
            description="Require auditors to check-out when leaving"
          />
        </div>
      </div>
      
      {/* Signature requirements */}
      <div className="space-y-4">
        <h4 className="font-medium">Signature Requirements</h4>
        <NumberInput
          label="Number of Signatures"
          value={localSettings.signatures || 0}
          onChange={handleSignatureChange}
          min={0}
          max={3}
          description="Number of required signatures for completion"
        />
        
        {(localSettings.signatures || 0) > 0 && (
          <ToggleSwitch
            label="Photo Signatures"
            checked={localSettings.selfieSignatures}
            onChange={(selfieSignatures) => onChange({ ...localSettings, selfieSignatures })}
            description="Require photo-based signatures"
          />
        )}
      </div>
    </div>
  );
};

Step 3: Site-Department Assignment System

Bulk Assignment Creation:

const BulkAssignmentCreator = ({ onAssignmentsAdd }: BulkAssignmentProps) => {
  const [selectedSites, setSelectedSites] = useState<SiteCompact[]>([]);
  const [selectedDepartments, setSelectedDepartments] = useState<Department[]>([]);
  
  // Generate all site-department combinations
  const createBulkAssignments = useCallback(() => {
    const newAssignments: SiteDepartmentAssignment[] = [];
    
    selectedSites.forEach(site => {
      selectedDepartments.forEach(department => {
        const assignmentId = `${site.siteID}-${department.departmentID}`;
        
        // Check for duplicates
        const exists = existingAssignments.some(a => a.id === assignmentId);
        if (exists) return;
        
        newAssignments.push({
          id: assignmentId,
          siteId: site.siteID,
          siteName: site.name,
          departmentId: department.departmentID,
          departmentName: department.name,
          timezone: site.timezone || 'UTC',
          hasOverrides: false,
        });
      });
    });
    
    onAssignmentsAdd(newAssignments);
    
    // Reset selections
    setSelectedSites([]);
    setSelectedDepartments([]);
    
    // Show success feedback
    toast.success(`Created ${newAssignments.length} assignments`);
  }, [selectedSites, selectedDepartments, existingAssignments, onAssignmentsAdd]);
  
  return (
    <div className="space-y-6">
      {/* Site selection with search and filtering */}
      <div className="space-y-4">
        <h4 className="font-medium">Select Sites</h4>
        <SiteMultiSelect
          value={selectedSites}
          onChange={setSelectedSites}
          placeholder="Search and select sites..."
        />
        {selectedSites.length > 0 && (
          <p className="text-sm text-muted-foreground">
            {selectedSites.length} site{selectedSites.length !== 1 ? 's' : ''} selected
          </p>
        )}
      </div>
      
      {/* Department selection */}
      <div className="space-y-4">
        <h4 className="font-medium">Select Departments</h4>
        <DepartmentMultiSelect
          value={selectedDepartments}
          onChange={setSelectedDepartments}
          placeholder="Search and select departments..."
        />
        {selectedDepartments.length > 0 && (
          <p className="text-sm text-muted-foreground">
            {selectedDepartments.length} department{selectedDepartments.length !== 1 ? 's' : ''} selected
          </p>
        )}
      </div>
      
      {/* Creation summary and action */}
      {selectedSites.length > 0 && selectedDepartments.length > 0 && (
        <div className="rounded-lg border p-4 bg-muted/50">
          <p className="text-sm font-medium mb-2">
            This will create {selectedSites.length × selectedDepartments.length} assignments
          </p>
          <Button onClick={createBulkAssignments} className="w-full">
            <Plus className="mr-2 h-4 w-4" />
            Create Assignments
          </Button>
        </div>
      )}
    </div>
  );
};

Assignment Override System:

const AssignmentOverridePanel = ({ assignment, globalSettings, onChange }: OverridePanelProps) => {
  const [overrides, setOverrides] = useState<Partial<ScheduleAdvancedSettings>>(
    assignment.overrides || {}
  );
  
  // Override change handler with inheritance logic
  const handleOverrideChange = useCallback((field: keyof ScheduleAdvancedSettings, value: any) => {
    setOverrides(prev => {
      const newOverrides = { ...prev, [field]: value };
      
      // Remove override if value matches global setting
      if (globalSettings[field] === value) {
        delete newOverrides[field];
      }
      
      // Clean up empty overrides object
      const hasOverrides = Object.keys(newOverrides).length > 0;
      
      // Notify parent of changes
      onChange(assignment.id, hasOverrides ? newOverrides : null);
      
      return newOverrides;
    });
  }, [assignment.id, globalSettings, onChange]);
  
  // Reset all overrides
  const resetOverrides = useCallback(() => {
    setOverrides({});
    onChange(assignment.id, null);
  }, [assignment.id, onChange]);
  
  return (
    <div className="space-y-6 p-4 border rounded-lg">
      <div className="flex items-center justify-between">
        <h5 className="font-medium">Assignment Overrides</h5>
        <Button 
          variant="ghost" 
          size="sm" 
          onClick={resetOverrides}
          disabled={Object.keys(overrides).length === 0}
        >
          Reset All
        </Button>
      </div>
      
      {/* Time overrides */}
      <div className="space-y-4">
        <h6 className="text-sm font-medium text-muted-foreground">Time Settings</h6>
        
        <TimeOverrideControl
          label="Start Time"
          globalValue={globalSettings.startTime}
          overrideValue={overrides.startTime}
          onChange={(value) => handleOverrideChange('startTime', value)}
        />
        
        <TimeOverrideControl
          label="End Time"
          globalValue={globalSettings.endTime}
          overrideValue={overrides.endTime}
          onChange={(value) => handleOverrideChange('endTime', value)}
        />
      </div>
      
      {/* Personnel overrides */}
      <div className="space-y-4">
        <h6 className="text-sm font-medium text-muted-foreground">Personnel</h6>
        
        <AuditorOverrideControl
          label="Assigned Auditors"
          globalAuditors={globalSettings.auditors}
          overrideAuditors={overrides.auditors}
          onChange={(auditors) => handleOverrideChange('auditors', auditors)}
          siteId={assignment.siteId}
          departmentId={assignment.departmentId}
        />
        
        <SupervisorOverrideControl
          label="Supervisor"
          globalSupervisor={globalSettings.supervisor}
          overrideSupervisor={overrides.supervisor}
          onChange={(supervisor) => handleOverrideChange('supervisor', supervisor)}
          siteId={assignment.siteId}
        />
      </div>
    </div>
  );
};

Step 4: Preview and Conflict Detection

Schedule Preview Generation:

const SchedulePreviewGenerator = ({ config }: PreviewGeneratorProps) => {
  const [previewData, setPreviewData] = useState<BulkSchedulePreview | null>(null);
  const [isGenerating, setIsGenerating] = useState(false);
  const [conflicts, setConflicts] = useState<ScheduleConflict[]>([]);
  
  // Generate preview with conflict detection
  const generatePreview = useCallback(async () => {
    if (!config.assignments?.length) return;
    
    setIsGenerating(true);
    
    try {
      // Parallel execution for performance
      const [conflictsResult, previewResult] = await Promise.all([
        bulkScheduleService.checkScheduleConflicts({
          siteIds: config.assignments.map(a => a.siteId),
          departmentIds: config.assignments.map(a => a.departmentId),
          startDate: config.startDate!,
          endDate: config.endDate!,
          questionnaireId: config.questionnaireIndexId!,
        }),
        bulkScheduleService.generateSchedulePreview(config),
      ]);
      
      setConflicts(conflictsResult.conflicts);
      setPreviewData(previewResult);
      
      // Show conflict summary
      if (conflictsResult.hasConflicts) {
        const errorCount = conflictsResult.conflicts.filter(c => c.severity === 'error').length;
        const warningCount = conflictsResult.conflicts.filter(c => c.severity === 'warning').length;
        
        toast.warning(
          `${errorCount} conflicts and ${warningCount} warnings detected. Please review before creating.`,
          { duration: 5000 }
        );
      }
    } catch (error) {
      console.error('Preview generation failed:', error);
      toast.error('Failed to generate preview. Please check your configuration.');
    } finally {
      setIsGenerating(false);
    }
  }, [config]);
  
  // Auto-generate when configuration changes
  useEffect(() => {
    if (config.assignments?.length) {
      generatePreview();
    }
  }, [config.assignments, config.startDate, config.endDate, generatePreview]);
  
  if (isGenerating) {
    return <PreviewLoadingSkeleton />;
  }
  
  if (!previewData) {
    return <PreviewEmptyState onGenerate={generatePreview} />;
  }
  
  return (
    <div className="space-y-6">
      {/* Preview summary */}
      <PreviewSummaryStats 
        totalSchedules={previewData.totalSchedules}
        assignments={config.assignments}
        conflicts={conflicts}
      />
      
      {/* Conflict alerts */}
      {conflicts.length > 0 && (
        <ConflictAlerts conflicts={conflicts} />
      )}
      
      {/* Detailed preview table */}
      <SchedulePreviewTable 
        schedules={previewData.schedules}
        globalSettings={config.globalSettings}
        onEditAssignment={(assignmentId) => onStepNavigation(2)} // Go back to assignments
      />
    </div>
  );
};

Draft Management System

Draft Lifecycle

Draft Creation and Updates:

const DraftManager = {
  // Create new draft
  async createDraft(config: BulkScheduleConfig, draftName: string): Promise<DraftSchedule> {
    const payload = this.transformConfigToDraftPayload(config, draftName);
    
    return draftScheduleService.saveDraftSchedule(payload);
  },
  
  // Update existing draft
  async updateDraft(draftId: string, config: BulkScheduleConfig, draftName?: string): Promise<DraftSchedule> {
    const payload = this.transformConfigToDraftPayload(config, draftName);
    payload.draftId = draftId;
    
    return draftScheduleService.saveDraftSchedule(payload);
  },
  
  // Auto-save functionality
  setupAutoSave(config: BulkScheduleConfig, draftId?: string, interval = 30000) {
    return setInterval(async () => {
      try {
        if (draftId && config.questionnaireIndexId && config.assignments?.length) {
          await this.updateDraft(draftId, config, `Auto-saved ${new Date().toLocaleTimeString()}`);
        }
      } catch (error) {
        console.warn('Auto-save failed:', error);
      }
    }, interval);
  },
  
  // Transform wizard config to draft format
  transformConfigToDraftPayload(config: BulkScheduleConfig, draftName: string): SaveDraftScheduleDto {
    return {
      draftName,
      draft: {
        schedule: {
          questionnaireIndexID: config.questionnaireIndexId,
          type: config.scheduleType,
          isRecurring: config.isRecurring,
          startDate: config.startDate,
          endDate: config.endDate,
          startTime: config.globalSettings.startTime || 0,
          endTime: config.globalSettings.endTime || 0,
          // ... additional transformations
        },
        globalAdvanceConfig: {
          supervisor: config.globalSettings.supervisor || '',
          defaultIssueOwner: config.globalSettings.defaultIssueOwner || '',
          // ... global settings transformation
        },
        siteDeptMapping: config.assignments.map(assignment => ({
          siteID: assignment.siteId,
          departmentID: assignment.departmentId,
          assignedAuditor: assignment.overrides?.auditors?.[0] || '',
          hasCustomConfig: assignment.hasOverrides,
          customAdvanceConfig: assignment.overrides ? {
            startTime: assignment.overrides.startTime,
            endTime: assignment.overrides.endTime,
            // ... override transformations
          } : undefined,
        })),
      },
    };
  },
};

Draft Publishing Workflow

Draft to Schedule Conversion:

const DraftPublisher = {
  // Pre-publish validation
  async validateForPublishing(draft: DraftSchedule): Promise<ValidationResult> {
    const errors: string[] = [];
    
    // Check required fields
    if (!draft.draft.schedule.questionnaireIndexID) {
      errors.push('Questionnaire is required for publishing');
    }
    
    if (!draft.draft.siteDeptMapping.length) {
      errors.push('At least one site-department assignment is required');
    }
    
    // Check for timing conflicts
    const conflicts = await bulkScheduleService.checkScheduleConflicts({
      siteIds: draft.draft.siteDeptMapping.map(m => m.siteID),
      departmentIds: draft.draft.siteDeptMapping.map(m => m.departmentID),
      startDate: draft.draft.schedule.startDate,
      endDate: draft.draft.schedule.endDate || '',
      questionnaireId: draft.draft.schedule.questionnaireIndexID,
    });
    
    if (conflicts.hasConflicts) {
      const errorConflicts = conflicts.conflicts.filter(c => c.severity === 'error');
      if (errorConflicts.length > 0) {
        errors.push(`${errorConflicts.length} schedule conflicts must be resolved before publishing`);
      }
    }
    
    return { isValid: errors.length === 0, errors };
  },
  
  // Publish draft with comprehensive error handling
  async publishDraft(draftId: string): Promise<{ success: boolean; scheduleIds: string[]; errors?: string[] }> {
    try {
      // Load and validate draft
      const draft = await draftScheduleService.getDraftScheduleById({ draftId });
      const validation = await this.validateForPublishing(draft);
      
      if (!validation.isValid) {
        return { success: false, scheduleIds: [], errors: validation.errors };
      }
      
      // Publish draft
      const result = await draftScheduleService.publishDraftSchedule({ draftId });
      
      // Clean up draft after successful publishing
      setTimeout(async () => {
        try {
          await draftScheduleService.deleteDraftSchedule({ draftId });
        } catch (cleanupError) {
          console.warn('Failed to cleanup draft after publishing:', cleanupError);
        }
      }, 1000);
      
      return { 
        success: true, 
        scheduleIds: result.data.scheduleIds 
      };
    } catch (error) {
      return { 
        success: false, 
        scheduleIds: [], 
        errors: [error instanceof Error ? error.message : 'Unknown error occurred'] 
      };
    }
  },
};

Conflict Detection and Resolution

Advanced Conflict Detection

Multi-Layer Conflict Analysis:

const ConflictDetector = {
  // Comprehensive conflict checking
  async analyzeScheduleConflicts(params: ScheduleConflictParams): Promise<ConflictAnalysisResult> {
    const conflicts: ScheduleConflict[] = [];
    
    // 1. Time overlap conflicts
    const timeConflicts = await this.detectTimeOverlaps(params);
    conflicts.push(...timeConflicts);
    
    // 2. Resource conflicts (auditor availability)
    const resourceConflicts = await this.detectResourceConflicts(params);
    conflicts.push(...resourceConflicts);
    
    // 3. Questionnaire compatibility
    const questionnaireConflicts = await this.detectQuestionnaireConflicts(params);
    conflicts.push(...questionnaireConflicts);
    
    // 4. Site capacity conflicts
    const capacityConflicts = await this.detectCapacityConflicts(params);
    conflicts.push(...capacityConflicts);
    
    return {
      conflicts,
      hasBlockingConflicts: conflicts.some(c => c.severity === 'error'),
      resolutionSuggestions: this.generateResolutionSuggestions(conflicts),
    };
  },
  
  // Time-based conflict detection
  async detectTimeOverlaps(params: ScheduleConflictParams): Promise<ScheduleConflict[]> {
    const conflicts: ScheduleConflict[] = [];
    
    for (const siteId of params.siteIds) {
      for (const departmentId of params.departmentIds) {
        // Check existing schedules for this site-department combination
        const existingSchedules = await this.getExistingSchedules(siteId, departmentId, params.dateRange);
        
        for (const existing of existingSchedules) {
          const hasOverlap = this.checkTimeOverlap(
            { start: params.startTime, end: params.endTime },
            { start: existing.startTime, end: existing.endTime }
          );
          
          if (hasOverlap) {
            conflicts.push({
              siteId,
              departmentId,
              existingScheduleId: existing.id,
              conflictType: 'overlap',
              severity: 'error',
              message: `Schedule overlaps with existing schedule "${existing.questionnaireName}" from ${this.formatTime(existing.startTime)} to ${this.formatTime(existing.endTime)}`,
              resolutionSuggestion: 'Adjust schedule timing or remove existing schedule',
            });
          }
        }
      }
    }
    
    return conflicts;
  },
  
  // Resource availability conflicts
  async detectResourceConflicts(params: ScheduleConflictParams): Promise<ScheduleConflict[]> {
    const conflicts: ScheduleConflict[] = [];
    
    // Check auditor availability across all assignments
    const auditorAssignments = new Map<string, string[]>();
    
    params.assignments.forEach(assignment => {
      const auditors = assignment.overrides?.auditors || params.globalAuditors || [];
      auditors.forEach(auditorId => {
        if (!auditorAssignments.has(auditorId)) {
          auditorAssignments.set(auditorId, []);
        }
        auditorAssignments.get(auditorId)!.push(`${assignment.siteId}-${assignment.departmentId}`);
      });
    });
    
    // Check for over-assignment
    for (const [auditorId, assignments] of auditorAssignments) {
      if (assignments.length > 1) {
        const auditorName = await this.getAuditorName(auditorId);
        conflicts.push({
          siteId: '',
          departmentId: '',
          existingScheduleId: '',
          conflictType: 'resource',
          severity: 'warning',
          message: `Auditor "${auditorName}" is assigned to ${assignments.length} simultaneous schedules`,
          resolutionSuggestion: 'Consider assigning additional auditors or adjusting schedule timing',
        });
      }
    }
    
    return conflicts;
  },
  
  // Generate resolution suggestions
  generateResolutionSuggestions(conflicts: ScheduleConflict[]): ResolutionSuggestion[] {
    const suggestions: ResolutionSuggestion[] = [];
    
    // Time conflict suggestions
    const timeConflicts = conflicts.filter(c => c.conflictType === 'overlap');
    if (timeConflicts.length > 0) {
      suggestions.push({
        type: 'timing_adjustment',
        title: 'Adjust Schedule Timing',
        description: 'Modify start/end times to avoid overlaps with existing schedules',
        action: 'edit_timing',
        affectedCount: timeConflicts.length,
      });
    }
    
    // Resource conflict suggestions
    const resourceConflicts = conflicts.filter(c => c.conflictType === 'resource');
    if (resourceConflicts.length > 0) {
      suggestions.push({
        type: 'resource_adjustment',
        title: 'Assign Additional Auditors',
        description: 'Add more auditors to handle simultaneous schedules',
        action: 'add_auditors',
        affectedCount: resourceConflicts.length,
      });
    }
    
    return suggestions;
  },
};

State Management Implementation

Jotai Atom Architecture

Atomic State Design:

// Base atoms for primitive state
export const currentWizardStepAtom = atom(0);
export const isDirtyAtom = atom(false);
export const selectedQuestionnaireAtom = atom<SelectedQuestionnaire | null>(null);
 
// Derived atoms for computed state
export const wizardProgressAtom = atom((get) => {
  const currentStep = get(currentWizardStepAtom);
  const totalSteps = 4;
  return {
    currentStep,
    totalSteps,
    percentage: Math.round((currentStep / (totalSteps - 1)) * 100),
    isComplete: currentStep === totalSteps - 1,
  };
});
 
// Complex derived state with validation
export const canProceedFromCurrentStepAtom = atom((get) => {
  const currentStep = get(currentWizardStepAtom);
  const questionnaire = get(selectedQuestionnaireAtom);
  const basicConfig = get(basicScheduleConfigAtom);
  const globalSettings = get(globalAdvancedSettingsAtom);
  const assignments = get(siteDepartmentAssignmentsAtom);
  
  switch (currentStep) {
    case 0: // Configure step
      return Boolean(
        questionnaire?.questionnaireIndexId
        && basicConfig.scheduleType
        && basicConfig.startDate
        && globalSettings.startTime !== undefined
        && globalSettings.endTime !== undefined
        && this.validateSchedulePattern(basicConfig)
      );
      
    case 1: // Advanced settings (optional)
      return true;
      
    case 2: // Assignments
      return assignments.length > 0;
      
    case 3: // Preview
      return true;
      
    default:
      return false;
  }
});
 
// Async atoms for API integration
export const questionnairesAtom = atom(async () => {
  return await bulkScheduleService.getQuestionnaires();
});
 
export const sitesAtom = atom(async (get) => {
  const searchParams = get(siteSearchParamsAtom);
  return await bulkScheduleService.getSitesForScheduling(searchParams);
});

Custom Hook Integration

Composite State Hooks:

export const useBulkScheduleWizard = () => {
  // Core state
  const [currentStep, setCurrentStep] = useAtom(currentWizardStepAtom);
  const [isDirty, setIsDirty] = useAtom(isDirtyAtom);
  const canProceed = useAtomValue(canProceedFromCurrentStepAtom);
  const progress = useAtomValue(wizardProgressAtom);
  
  // Configuration state
  const [questionnaire, setQuestionnaire] = useAtom(selectedQuestionnaireAtom);
  const [basicConfig, setBasicConfig] = useAtom(basicScheduleConfigAtom);
  const [globalSettings, setGlobalSettings] = useAtom(globalAdvancedSettingsAtom);
  const [assignments, setAssignments] = useAtom(siteDepartmentAssignmentsAtom);
  
  // Derived complete configuration
  const completeConfig = useAtomValue(bulkScheduleConfigAtom);
  
  // Navigation methods
  const goToStep = useCallback((stepIndex: number) => {
    if (stepIndex >= 0 && stepIndex < 4 && (stepIndex <= currentStep + 1 || canProceed)) {
      setCurrentStep(stepIndex);
    }
  }, [currentStep, canProceed, setCurrentStep]);
  
  const goNext = useCallback(() => {
    if (canProceed && currentStep < 3) {
      setCurrentStep(prev => prev + 1);
    }
  }, [canProceed, currentStep, setCurrentStep]);
  
  const goPrevious = useCallback(() => {
    if (currentStep > 0) {
      setCurrentStep(prev => prev - 1);
    }
  }, [currentStep, setCurrentStep]);
  
  // Configuration update methods
  const updateConfig = useCallback((updates: Partial<BulkScheduleConfig>) => {
    setIsDirty(true);
    
    // Update specific sections based on update keys
    if ('questionnaireIndexId' in updates || 'questionnaireName' in updates) {
      setQuestionnaire({
        questionnaireIndexId: updates.questionnaireIndexId || questionnaire?.questionnaireIndexId || '',
        questionnaireName: updates.questionnaireName || questionnaire?.questionnaireName || '',
      });
    }
    
    // Update basic configuration
    const basicFields = ['scheduleType', 'isRecurring', 'startDate', 'endDate', 'isAdhocOnly', 'daysOfWeek', 'datesOfMonth', 'customDates'];
    const basicUpdates = Object.keys(updates)
      .filter(key => basicFields.includes(key))
      .reduce((acc, key) => ({ ...acc, [key]: updates[key as keyof BulkScheduleConfig] }), {});
    
    if (Object.keys(basicUpdates).length > 0) {
      setBasicConfig(prev => ({ ...prev, ...basicUpdates }));
    }
    
    // Update global settings
    if (updates.globalSettings) {
      setGlobalSettings(prev => ({ ...prev, ...updates.globalSettings }));
    }
    
    // Update assignments
    if (updates.assignments) {
      setAssignments(updates.assignments);
    }
  }, [questionnaire, setQuestionnaire, setBasicConfig, setGlobalSettings, setAssignments, setIsDirty]);
  
  // Reset all wizard state
  const resetWizard = useCallback(() => {
    setCurrentStep(0);
    setIsDirty(false);
    setQuestionnaire(null);
    setBasicConfig({});
    setGlobalSettings({});
    setAssignments([]);
  }, [setCurrentStep, setIsDirty, setQuestionnaire, setBasicConfig, setGlobalSettings, setAssignments]);
  
  return {
    // State
    currentStep,
    isDirty,
    canProceed,
    progress,
    completeConfig,
    
    // Configuration sections
    questionnaire,
    basicConfig,
    globalSettings,
    assignments,
    
    // Actions
    goToStep,
    goNext,
    goPrevious,
    updateConfig,
    resetWizard,
  };
};

The features implementation provides robust, user-friendly workflows for complex scheduling scenarios while maintaining excellent performance and error handling throughout the application lifecycle.


Integration & APIs

Overview

The bulk-schedule module integrates with multiple Nimbly API services to provide comprehensive scheduling functionality. It follows RESTful API conventions and implements robust error handling, authentication, and data validation patterns.

API Architecture

graph TB
    A[Bulk Schedule Module] --> B[API Client Layer]
    
    B --> C[Main Nimbly API]
    B --> D[Bulk Operations API]
    B --> E[Draft Storage API]
    B --> F[Functions API]
    
    C --> G[Questionnaires Service]
    C --> H[Sites Service]
    C --> I[Departments Service]
    C --> J[Users Service]
    
    D --> K[Schedule Creation Service]
    D --> L[Conflict Detection Service]
    D --> M[Validation Service]
    
    E --> N[Draft CRUD Service]
    E --> O[Draft Publishing Service]
    
    F --> P[Preview Generation Service]
    F --> Q[Template Management Service]

Authentication and Authorization

Firebase Authentication Integration

The module uses Firebase Authentication for user identity management:

// Authentication hook integration
export const useBulkScheduleAuth = () => {
  const { apiUser, isLoading, error } = auth.useSession();
  
  // Ensure user has required organization context
  if (!apiUser?.organizationID) {
    throw new Error('User must belong to an organization to access bulk scheduling');
  }
  
  // Check for required permissions
  const hasSchedulingPermissions = apiUser.permissions?.includes('schedule:write') || 
                                 apiUser.role === 'admin';
  
  return {
    user: apiUser,
    organizationId: apiUser.organizationID,
    userId: apiUser.userID,
    hasPermissions: hasSchedulingPermissions,
    isLoading,
    error,
  };
};
 
// API client authentication interceptor
export class AuthenticatedApiClient {
  constructor() {
    this.api.interceptors.request.use((config) => {
      const token = auth.getCurrentToken();
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    });
    
    this.api.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.response?.status === 401) {
          auth.signOut();
          window.location.href = '/auth/signin';
        }
        return Promise.reject(error);
      }
    );
  }
}

Core API Endpoints

Questionnaire Management

Endpoint: GET /questionnaires/questionnaireIndexes/minified

Purpose: Retrieve available questionnaires with optimized field selection for scheduling.

Parameters:

  • props[]: Array of field names to include in response

Response Format:

{
  message: "SUCCESS",
  data: [
    {
      questionnaireIndexID: "string",
      organizationID: "string", 
      title: "string",
      status: "active" | "inactive",
      dateCreated: "ISO8601",
      dateUpdated: "ISO8601",
      disabled: boolean,
      type: "string",
      modifiedBy: "string",
      tags: string[],
      autoAssignment: boolean,
      questionCount: number
    }
  ]
}

Repository Implementation:

async getQuestionnaires(): Promise<QuestionnairesApiResponseDto> {
  return this.withMonitoring('get_questionnaires', 'bulk_schedule', async () => {
    const questionnaireProps = [
      'questionnaireID', 'organizationID', 'title', 'status',
      'dateCreated', 'dateUpdated', 'disabled', 'type',
      'questionnaireIndexID', 'modifiedBy', 'tags', 
      'autoAssignment', 'questionCount'
    ];
 
    const queryParams = new URLSearchParams();
    questionnaireProps.forEach(prop => queryParams.append('props', prop));
 
    const response = await this.client.api.get<QuestionnairesApiResponseDto>(
      `/questionnaires/questionnaireIndexes/minified?${queryParams.toString()}`
    );
 
    if (!response.data || response.data.message !== 'SUCCESS') {
      throw new Error('Failed to fetch questionnaires');
    }
 
    return response.data;
  });
}

Site Management

Endpoint: GET /sites/compact

Purpose: Retrieve sites optimized for scheduling with filtering capabilities.

Parameters:

  • search: Text search across site names
  • departmentIds: Comma-separated department IDs for filtering
  • includeDisabled: Include inactive sites
  • limit: Maximum number of results
  • offset: Pagination offset

Response Format:

{
  message: "SUCCESS",
  data: [
    {
      siteID: "string",
      name: "string",
      organizationID: "string",
      address: "string",
      city: "string",
      country: "string",
      timezone: "string"
    }
  ]
}

Department Management

Endpoint: GET /departments

Purpose: Retrieve all departments for the organization.

Response Format:

{
  message: "SUCCESS", 
  data: [
    {
      departmentID: "string",
      name: "string",
      description: "string",
      email: "string",
      status: "active" | "inactive",
      organizationID: "string",
      createdAt: "ISO8601",
      updatedAt: "ISO8601"
    }
  ]
}

User Management

Endpoint: GET /users

Purpose: Retrieve users for auditor assignment with role and location filtering.

Parameters:

  • siteIds[]: Filter by site access
  • departmentIds[]: Filter by department access
  • role: Filter by user role
  • search: Text search across user names

Response Format:

{
  message: "SUCCESS",
  data: {
    [userID: string]: {
      userID: "string",
      firstName: "string", 
      lastName: "string",
      email: "string",
      role: "string",
      organizationID: "string",
      sites: string[],
      departments: string[],
      isActive: boolean
    }
  }
}

Schedule Management APIs

Conflict Detection

Endpoint: POST /schedules/check-conflicts

Purpose: Detect scheduling conflicts before creation.

Request Body:

{
  siteIds: string[],
  departmentIds: string[],
  startDate: "YYYY-MM-DD",
  endDate: "YYYY-MM-DD", 
  questionnaireId: "string"
}

Response Format:

{
  conflicts: [
    {
      siteId: "string",
      siteName: "string",
      departmentId: "string", 
      departmentName: "string",
      existingScheduleId: "string",
      conflictType: "overlap" | "duplicate" | "questionnaire",
      message: "string",
      severity: "error" | "warning"
    }
  ],
  hasConflicts: boolean
}

Schedule Creation

Endpoint: PUT /v1.0/schedules/sites/v2/bulk

Purpose: Create multiple schedules using the v2 bulk API.

Request Body:

{
  data: {
    schedule: {
      isEom: boolean,
      startDate: "YYYY-MM-DD",
      endDate: "YYYY-MM-DD",
      startTime: number, // minutes from midnight
      endTime: number,   // minutes from midnight
      type: "daily" | "weekly" | "monthly" | "custom",
      isRecurring: boolean,
      questionnaireIndexID: "string",
      daysOfWeek: number[], // 0-6, Sunday-Saturday
      datesOfMonth: number[], // 1-31
      datesCustomV2: string[], // ISO date strings
      isAdhocOnly: boolean,
      hasDeadline: boolean,
      offsetTime: number,
      reminderTime: number,
      withCalendarSync: boolean,
      createdBy: "string"
    },
    globalAdvanceConfig: {
      supervisor: "string",
      defaultIssueOwner: "string", 
      hasStrictTime: boolean,
      enforceCheckIn: boolean,
      enforceCheckOut: boolean,
      signatures: number,
      selfieSignatures: Array<{ title: string, path: string }>,
      allowAdhoc: boolean,
      reminderPeriod: "hours" | "days" | "weeks",
      emailTargets: string[],
      scheduleActivePeriod: {
        periodLength: number,
        periodUnit: "day" | "week" | "month",
        startActiveDate: "YYYY-MM-DD",
        endActiveDate: "YYYY-MM-DD",
        startActiveAt: "string"
      }
    },
    siteDeptMapping: [
      {
        siteID: "string",
        departmentID: "string",
        assignedAuditor: string[], // Array of auditor IDs
        hasCustomConfig: boolean,
        customAdvanceConfig?: {
          startTime: number,
          endTime: number,
          hasStrictTime: boolean,
          enforceCheckIn: boolean,
          enforceCheckOut: boolean,
          signatures: number,
          selfieSignatures: Array<{ title: string, path: string }>,
          emailTargets: string[],
          reminderPeriod: "hours" | "days" | "weeks",
          allowAdhoc: boolean,
          supervisor: "string",
          defaultIssueOwner: "string"
        }
      }
    ]
  }
}

Response Format:

{
  data: unknown[],
  message: "SUCCESS" | "FAILED",
  full: boolean
}

Draft Management APIs

Draft Retrieval

Endpoint: GET /v1.0/schedules/sites/draft

Purpose: Retrieve paginated list of draft schedules with search and filtering.

Parameters:

  • page: Page number (1-based)
  • limit: Items per page
  • search: Text search across draft names
  • sortBy: Sort field (updatedAt, createdAt, draftName)
  • sortOrder: Sort direction (asc, desc)

Response Format:

{
  message: "SUCCESS",
  data: {
    data: [
      {
        _id: "string",
        organizationID: "string",
        draft: {
          schedule: {
            questionnaireIndexID: "string",
            type: "daily" | "weekly" | "monthly" | "custom",
            isRecurring: boolean,
            startDate: "YYYY-MM-DD", 
            endDate: "YYYY-MM-DD",
            startTime: number,
            endTime: number,
            firstScheduleStart: "ISO8601",
            firstScheduleEnd: "ISO8601",
            daysOfWeek: number[],
            datesOfMonth: number[],
            datesCustomV2: Array<string | { date: string }>,
            isAdhocOnly: boolean,
            isEom: boolean,
            hasDeadline: boolean,
            offsetTime: number,
            reminderTime: number,
            withCalendarSync: boolean,
            repeatingType: "string",
            occurenceNumber: number,
            repeatingEndDate: "ISO8601"
          },
          globalAdvanceConfig: {
            supervisor: "string",
            defaultIssueOwner: "string",
            hasStrictTime: boolean,
            enforceCheckIn: boolean,
            enforceCheckOut: boolean,
            signatures: number,
            selfieSignatures: Array<{ title: string, path: string }>,
            allowAdhoc: boolean,
            reminderPeriod: "hours" | "days" | "weeks",
            emailTargets: string[],
            scheduleActivePeriod: {
              periodLength: number,
              periodUnit: "day" | "week" | "month",
              startActiveDate: "YYYY-MM-DD",
              endActiveDate: "YYYY-MM-DD",
              startActiveAt: "string",
              endActiveAt: null
            }
          },
          siteDeptMapping: [
            {
              siteID: "string",
              departmentID: "string", 
              assignedAuditor: "string",
              hasCustomConfig: boolean,
              customAdvanceConfig?: {
                startTime: number,
                endTime: number,
                hasStrictTime: boolean,
                enforceCheckIn: boolean,
                enforceCheckOut: boolean,
                signatures: number,
                selfieSignatures: Array<{ title: string, path: string }>,
                emailTargets: string[],
                reminderPeriod: "hours" | "days" | "weeks",
                allowAdhoc: boolean,
                supervisor: "string",
                defaultIssueOwner: "string"
              }
            }
          ]
        },
        createdBy: "string",
        updatedBy: "string", 
        draftName: "string",
        draftId: "string",
        createdAt: "ISO8601",
        updatedAt: "ISO8601",
        __v: number
      }
    ],
    totalCount: number,
    page: number,
    limit: number,
    totalPages: number
  }
}

Draft Creation/Update

Endpoint: POST /v1.0/schedules/sites/draft

Purpose: Create new draft or update existing draft.

Request Body:

{
  draftName: "string",
  draftId?: "string", // Include for updates
  draft: {
    schedule: { /* Schedule configuration */ },
    globalAdvanceConfig: { /* Global settings */ },
    siteDeptMapping: [ /* Site-department mappings */ ]
  }
}

Response Format:

{
  message: "SUCCESS",
  data: {
    /* Draft object same as retrieval format */
  }
}

Draft Publishing

Endpoint: POST /v1.0/schedules/sites/draft/{draftId}/publish

Purpose: Convert draft to active schedules.

Response Format:

{
  message: "SUCCESS",
  data: {
    scheduleIds: string[]
  }
}

Draft Deletion

Endpoint: DELETE /v1.0/schedules/sites/draft/{draftId}

Purpose: Remove draft from system.

Response Format:

{
  message: "SUCCESS",
  data: {}
}

Complete API Endpoint Reference

EndpointMethodPurposeRepository MethodFile Link
/questionnaires/questionnaireIndexes/minifiedGETGet available questionnairesgetQuestionnaires()bulk-schedule-repository.ts:52
/sites/compactGETGet sites for schedulinggetSitesForScheduling()bulk-schedule-repository.ts:78
/departmentsGETGet all departmentsgetDepartments()bulk-schedule-repository.ts:115
/usersGETGet users for auditor selectiongetUsersForAuditors()bulk-schedule-repository.ts:130
/schedules/check-conflictsPOSTCheck for schedule conflictscheckScheduleConflicts()bulk-schedule-repository.ts:171
/schedules/bulk/previewPOSTGenerate schedule previewgeneratePreview()bulk-schedule-repository.ts:190
/v1.0/schedules/sites/v2/bulkPUTCreate bulk schedulescreateBulkSchedules()bulk-schedule-repository.ts:220
/schedules/bulk/validatePOSTValidate bulk configurationvalidateBulkSchedule()bulk-schedule-repository.ts:259
/schedules/bulk/templatesGETGet schedule templatesgetBulkScheduleTemplates()bulk-schedule-repository.ts:285
/schedules/bulk/templatesPOSTSave schedule templatesaveBulkScheduleTemplate()bulk-schedule-repository.ts:320
/schedules/bulk/templates/{id}DELETEDelete schedule templatedeleteBulkScheduleTemplate()bulk-schedule-repository.ts:355
/v1.0/schedules/sites/draftGETGet draft schedulesgetDraftSchedules()draft-schedule-repository.ts:25
/v1.0/schedules/sites/draftPOSTSave draft schedulesaveDraftSchedule()draft-schedule-repository.ts:75
/v1.0/schedules/sites/draft/{id}GETGet draft by IDgetDraftScheduleById()draft-schedule-repository.ts:55
/v1.0/schedules/sites/draft/{id}PUTUpdate draft scheduleupdateDraftSchedule()draft-schedule-repository.ts:90
/v1.0/schedules/sites/draft/{id}DELETEDelete draft scheduledeleteDraftSchedule()draft-schedule-repository.ts:105
/v1.0/schedules/sites/draft/{id}/publishPOSTPublish draft schedulepublishDraftSchedule()draft-schedule-repository.ts:120

Error Handling Patterns

Standardized Error Response Format

All API responses follow the Nimbly standard format:

// Success Response
{
  message: "SUCCESS",
  data: T // Type-specific response data
}
 
// Error Response  
{
  message: "ERROR",
  error: "Error description",
  code?: "ERROR_CODE",
  details?: {
    field: "validation error details"
  }
}

Repository-Level Error Handling

async performApiOperation<T>(operation: () => Promise<T>): Promise<T> {
  return this.withMonitoring('operation_name', 'feature', async () => {
    try {
      const response = await operation();
      
      // Validate response format
      if (!response?.data || response.data.message !== 'SUCCESS') {
        throw new Error(response?.data?.error || 'Operation failed');
      }
      
      return response.data;
    } catch (error) {
      // Log error with context
      console.error('API operation failed:', {
        operation: 'operation_name',
        error: error.message,
        stack: error.stack
      });
      
      // Transform API errors to user-friendly messages
      if (error.response?.status === 400) {
        throw new Error('Invalid request. Please check your input.');
      } else if (error.response?.status === 403) {
        throw new Error('You do not have permission to perform this action.');
      } else if (error.response?.status >= 500) {
        throw new Error('Server error. Please try again later.');
      }
      
      // Re-throw with original message if no specific handling
      throw error;
    }
  });
}

Service-Level Error Enrichment

async performBusinessOperation(params: OperationParams): Promise<OperationResult> {
  return this.withMonitoring('business_operation', 'feature', async () => {
    try {
      return await this.repository.performOperation(params);
    } catch (error) {
      this.captureServiceError(error as Error, 'business_operation', 'feature', 'medium', {
        operation_params: params,
      });
      
      // Add business context to error
      throw new Error(
        this.i18n._(msg`Failed to perform operation: ${error.message}`)
      );
    }
  }, 'medium', {
    param_count: Object.keys(params).length,
  });
}

Rate Limiting and Performance

Caching Strategy

The module implements intelligent caching through TanStack Query:

// Long-lived data (questionnaires, departments)
export const questionnairesQuery = queryOptions({
  queryKey: ['questionnaires'],
  queryFn: () => bulkScheduleService.getQuestionnaires(),
  staleTime: 10 * 60 * 1000, // 10 minutes
  gcTime: 15 * 60 * 1000,    // 15 minutes
  retry: 2,
});
 
// Medium-lived data (sites, users) 
export const sitesQuery = queryOptions({
  queryKey: ['sites'],
  queryFn: () => bulkScheduleService.getSitesForScheduling(),
  staleTime: 5 * 60 * 1000,  // 5 minutes
  gcTime: 10 * 60 * 1000,    // 10 minutes
  retry: 2,
});
 
// Short-lived data (conflicts, previews)
export const conflictsQuery = queryOptions({
  queryKey: ['conflicts', params],
  queryFn: () => bulkScheduleService.checkScheduleConflicts(params),
  staleTime: 0,    // Always fresh
  gcTime: 0,       // No cache
  retry: 1,
});

Request Optimization

// Batch API calls where possible
export const useBulkScheduleData = () => {
  return useQueries({
    queries: [
      questionnairesQuery(),
      sitesQuery(),
      departmentsQuery(),
      usersQuery(),
    ],
    combine: (results) => ({
      data: {
        questionnaires: results[0].data,
        sites: results[1].data, 
        departments: results[2].data,
        users: results[3].data,
      },
      isLoading: results.some(r => r.isLoading),
      error: results.find(r => r.error)?.error,
    })
  });
};
 
// Debounced search to reduce API calls
export const useDebouncedSiteSearch = (searchTerm: string, delay = 300) => {
  const [debouncedTerm, setDebouncedTerm] = useState(searchTerm);
  
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedTerm(searchTerm);
    }, delay);
    
    return () => clearTimeout(timer);
  }, [searchTerm, delay]);
  
  return useQuery({
    ...sitesQuery({ search: debouncedTerm }),
    enabled: debouncedTerm.length >= 2, // Minimum search length
  });
};

Integration Testing Patterns

API Mock Integration

// Test utilities for API mocking
export const mockBulkScheduleApi = {
  questionnaires: {
    success: (): QuestionnairesApiResponseDto => ({
      message: 'SUCCESS',
      data: [
        {
          questionnaireIndexID: 'q1',
          title: 'Test Questionnaire',
          status: 'active',
          questionCount: 5,
          organizationID: 'org1',
          dateCreated: '2024-01-01T00:00:00Z',
          dateUpdated: '2024-01-01T00:00:00Z',
          disabled: false,
          type: 'standard',
          modifiedBy: 'user1',
          tags: [],
          autoAssignment: false,
        }
      ]
    }),
    
    error: () => ({
      message: 'ERROR',
      error: 'Failed to fetch questionnaires'
    })
  },
  
  createSchedules: {
    success: (count: number = 1) => ({
      data: Array(count).fill(null),
      message: 'SUCCESS',
      full: true
    }),
    
    failure: () => ({
      data: [],
      message: 'FAILED',
      full: false
    })
  }
};
 
// Integration test example
describe('Bulk Schedule Creation', () => {
  beforeEach(() => {
    // Setup API mocks
    vi.mocked(bulkScheduleRepository.getQuestionnaires)
      .mockResolvedValue(mockBulkScheduleApi.questionnaires.success());
    
    vi.mocked(bulkScheduleRepository.createBulkSchedules)
      .mockResolvedValue(mockBulkScheduleApi.createSchedules.success(3));
  });
  
  it('should create schedules successfully', async () => {
    const config = createTestBulkScheduleConfig();
    const result = await bulkScheduleService.createBulkSchedulesFromConfig(config, 'user1');
    
    expect(result.success).toBe(true);
    expect(result.createdCount).toBe(3);
  });
});

The integration layer provides robust, well-tested connectivity between the bulk-schedule module and Nimbly’s API ecosystem, ensuring reliable data exchange and comprehensive error handling throughout all user workflows.


Internationalization & Localization

Overview

The bulk-schedule module implements comprehensive internationalization (i18n) using the Lingui framework, supporting multiple languages and regional preferences. The implementation ensures all user-facing text is translatable while maintaining developer-friendly APIs.

Lingui Framework Integration

Configuration Setup

The module uses Lingui’s macro-based approach for compile-time optimization:

File: lingui.config.js

module.exports = {
  locales: ['en', 'es', 'pt', 'id', 'ko', 'th', 'cmn'],
  sourceLocale: 'en',
  catalogs: [
    {
      path: 'src/locales/{locale}/messages',
      include: ['src/'],
    },
  ],
  format: 'po',
  compileNamespace: 'es',
};

Language Support

The module supports 7 languages with region-specific adaptations:

LanguageCodeRegionFile PathStatus
EnglishenUnited Statessrc/locales/en.poComplete
SpanishesLatin Americasrc/locales/es.poComplete
PortugueseptBrazilsrc/locales/pt.poComplete
IndonesianidIndonesiasrc/locales/id.poComplete
KoreankoSouth Koreasrc/locales/ko.poComplete
ThaithThailandsrc/locales/th.poComplete
Chinese (Mandarin)cmnChina/Taiwansrc/locales/cmn.poComplete

Translation Implementation Patterns

Static Text Translation

Basic Message Translation:

import { Trans, useLingui } from '@lingui/react/macro';
import { msg } from '@lingui/core/macro';
 
// React components
function BulkScheduleWizard() {
  return (
    <div>
      <h1><Trans>Create Bulk Schedule</Trans></h1>
      <p><Trans>Configure schedules across multiple sites and departments.</Trans></p>
    </div>
  );
}
 
// JavaScript functions
function validateSchedule() {
  const { i18n } = useLingui();
  const errorMessage = i18n._(msg`Invalid schedule configuration`);
  throw new Error(errorMessage);
}

Dynamic Text Translation

Parameterized Messages:

import { Trans, Plural } from '@lingui/react/macro';
import { msg, plural } from '@lingui/core/macro';
 
// Component with dynamic values
function ScheduleCreationSummary({ createdCount }: { createdCount: number }) {
  return (
    <div>
      <Trans>
        Successfully created {createdCount} schedule{createdCount !== 1 ? 's' : ''}
      </Trans>
      
      {/* Alternative using Plural component */}
      <Plural
        value={createdCount}
        one="# schedule created"
        other="# schedules created"
      />
    </div>
  );
}
 
// Service with parameterized messages
export class BulkScheduleService {
  private getConflictMessage(conflictCount: number): string {
    return this.i18n._(
      plural(conflictCount, {
        one: '# conflict detected',
        other: '# conflicts detected',
      })
    );
  }
}

Complex Message Formatting

Date and Time Localization:

import { formatTimeRange } from '../_lib/bulk-schedule-utils';
 
export function formatTimeRange(
  startTime?: number, 
  endTime?: number, 
  i18n?: I18n
): string {
  if (startTime === undefined || endTime === undefined) {
    return i18n ? i18n._(msg`Not set`) : 'Not set';
  }
 
  const formatTime = (minutes: number): string => {
    const hours = Math.floor(minutes / 60);
    const mins = minutes % 60;
    
    // Use 24-hour format for some locales
    const use24Hour = i18n?.locale && ['th', 'cmn'].includes(i18n.locale);
    
    if (use24Hour) {
      return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`;
    }
    
    const period = hours >= 12 ? 'PM' : 'AM';
    const displayHours = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
    return `${displayHours}:${mins.toString().padStart(2, '0')} ${period}`;
  };
 
  return `${formatTime(startTime)} - ${formatTime(endTime)}`;
}

Context-Aware Translations

Business Domain Context:

// Domain-specific translations with context
export const DraftScheduleBusinessRules = {
  getScheduleTypeLabel: (
    type: ScheduleType, 
    i18n?: { _: (msg: { id: string }) => string }
  ): string => {
    const labels: Record<ScheduleType, ReturnType<typeof msg>> = {
      daily: msg`Daily`,
      weekly: msg`Weekly`, 
      monthly: msg`Monthly`,
      custom: msg`Custom`,
    };
    
    const label = labels[type];
    return label && i18n ? i18n._(label) : type;
  },
 
  getScheduleStatusDisplay: (
    isRecurring: boolean,
    isAdhocOnly: boolean,
    i18n?: { _: (msg: { id: string }) => string }
  ): string => {
    if (isAdhocOnly) {
      return i18n ? i18n._(msg`Ad-hoc Only`) : 'Ad-hoc Only';
    }
    
    if (isRecurring) {
      return i18n ? i18n._(msg`Recurring Schedule`) : 'Recurring Schedule';
    }
    
    return i18n ? i18n._(msg`One-time Schedule`) : 'One-time Schedule';
  },
};

Translation Key Organization

Systematic Key Structure

The translation keys follow a hierarchical structure for maintainability:

bulk-schedule.
├── wizard.
│   ├── steps.
│   │   ├── configure.title
│   │   ├── configure.description
│   │   ├── advanced.title
│   │   └── advanced.description
│   ├── navigation.
│   │   ├── next
│   │   ├── previous
│   │   └── finish
│   └── validation.
│       ├── questionnaire-required
│       ├── schedule-type-required
│       └── assignments-required
├── drafts.
│   ├── table.
│   │   ├── name
│   │   ├── questionnaire
│   │   ├── schedule-mode
│   │   ├── start-date
│   │   └── actions
│   ├── actions.
│   │   ├── edit
│   │   ├── delete
│   │   ├── publish
│   │   └── duplicate
│   └── messages.
│       ├── created-successfully
│       ├── updated-successfully
│       └── deleted-successfully
└── conflicts.
    ├── types.
    │   ├── overlap
    │   ├── duplicate
    │   └── resource
    ├── severity.
    │   ├── error
    │   └── warning
    └── resolutions.
        ├── adjust-timing
        ├── remove-existing
        └── add-resources

Sample Translation Files

English (Source Language)

File: src/locales/bulk-schedule/en.po

# Wizard Steps
msgid "Configure Schedule"
msgstr "Configure Schedule"
 
msgid "Select questionnaire and set schedule type"
msgstr "Select questionnaire and set schedule type"
 
msgid "Advanced Settings"
msgstr "Advanced Settings"
 
msgid "Configure default auditors and features"
msgstr "Configure default auditors and features"
 
# Validation Messages
msgid "Questionnaire is required"
msgstr "Questionnaire is required"
 
msgid "At least one day of week must be selected"
msgstr "At least one day of week must be selected"
 
msgid "At least one site-department assignment is required"
msgstr "At least one site-department assignment is required"
 
# Schedule Types
msgid "Daily"
msgstr "Daily"
 
msgid "Weekly"
msgstr "Weekly"
 
msgid "Monthly"
msgstr "Monthly"
 
msgid "Custom"
msgstr "Custom"
 
# Time Formatting
msgid "Not set"
msgstr "Not set"
 
# Conflict Messages
msgid "Schedule overlaps with existing schedule"
msgstr "Schedule overlaps with existing schedule"
 
msgid "Adjust schedule timing or remove existing schedule"
msgstr "Adjust schedule timing or remove existing schedule"
 
# Success Messages
msgid "Draft saved successfully!"
msgstr "Draft saved successfully!"
 
msgid "Schedules created successfully!"
msgstr "Schedules created successfully!"
 
# Plural Forms
msgid "{0} schedule created"
msgid_plural "{0} schedules created"
msgstr[0] "{0} schedule created"
msgstr[1] "{0} schedules created"

Spanish Translation

File: src/locales/bulk-schedule/es.po

# Wizard Steps
msgid "Configure Schedule"
msgstr "Configurar Horario"
 
msgid "Select questionnaire and set schedule type"
msgstr "Seleccionar cuestionario y establecer tipo de horario"
 
msgid "Advanced Settings"
msgstr "Configuración Avanzada"
 
msgid "Configure default auditors and features"
msgstr "Configurar auditores predeterminados y funciones"
 
# Validation Messages
msgid "Questionnaire is required"
msgstr "Se requiere un cuestionario"
 
msgid "At least one day of week must be selected"
msgstr "Debe seleccionar al menos un día de la semana"
 
# Schedule Types
msgid "Daily"
msgstr "Diario"
 
msgid "Weekly"
msgstr "Semanal"
 
msgid "Monthly"
msgstr "Mensual"
 
msgid "Custom"
msgstr "Personalizado"
 
# Plural Forms (Spanish has complex plural rules)
msgid "{0} schedule created"
msgid_plural "{0} schedules created"
msgstr[0] "{0} horario creado"
msgstr[1] "{0} horarios creados"

Thai Translation (Complex Script)

File: src/locales/bulk-schedule/th.po

# Wizard Steps
msgid "Configure Schedule"
msgstr "กำหนดค่าตารางเวลา"
 
msgid "Select questionnaire and set schedule type"
msgstr "เลือกแบบสอบถามและตั้งค่าประเภทตารางเวลา"
 
msgid "Advanced Settings"
msgstr "การตั้งค่าขั้นสูง"
 
# Schedule Types
msgid "Daily"
msgstr "รายวัน"
 
msgid "Weekly"
msgstr "รายสัปดาห์"
 
msgid "Monthly"
msgstr "รายเดือน"
 
msgid "Custom"
msgstr "กำหนดเอง"
 
# Thai doesn't have plural forms like English
msgid "{0} schedule created"
msgid_plural "{0} schedules created"
msgstr[0] "สร้างตารางเวลา {0} รายการแล้ว"

Locale-Specific Adaptations

Date and Number Formatting

import { format, parseISO } from 'date-fns';
import { enUS, es, pt, th, ko, zhCN } from 'date-fns/locale';
 
const localeMap = {
  en: enUS,
  es: es,
  pt: pt, 
  th: th,
  ko: ko,
  cmn: zhCN,
};
 
export function formatLocalizedDate(
  dateString: string,
  formatString: string,
  locale: string
): string {
  const date = parseISO(dateString);
  const dateLocale = localeMap[locale as keyof typeof localeMap] || enUS;
  
  return format(date, formatString, { locale: dateLocale });
}
 
// Number formatting with locale awareness
export function formatLocalizedNumber(
  value: number,
  locale: string,
  options?: Intl.NumberFormatOptions
): string {
  return new Intl.NumberFormat(locale, options).format(value);
}

Cultural Considerations

// Time format preferences by culture
export function getTimeFormat(locale: string): '12h' | '24h' {
  const use24Hour = ['th', 'cmn', 'ko'].includes(locale);
  return use24Hour ? '24h' : '12h';
}
 
// Week start day by culture
export function getWeekStartDay(locale: string): number {
  // 0 = Sunday, 1 = Monday
  const mondayFirst = ['cmn', 'ko', 'th'].includes(locale);
  return mondayFirst ? 1 : 0;
}
 
// Date format preferences
export function getDateFormat(locale: string): string {
  const formats: Record<string, string> = {
    en: 'MM/dd/yyyy',
    es: 'dd/MM/yyyy', 
    pt: 'dd/MM/yyyy',
    th: 'dd/MM/yyyy',
    ko: 'yyyy.MM.dd',
    cmn: 'yyyy/MM/dd',
  };
  
  return formats[locale] || formats.en;
}

Package Dependencies

Core Dependencies

The bulk-schedule module relies on several key packages for its functionality:

PackageVersionPurposeDocumentation
React Framework
react^18.2.0Core UI frameworkReact Docs
next^15.0.0Full-stack React framework with App RouterNext.js Docs
TypeScript
typescript^5.3.0Type safety and developer experienceTypeScript Docs
State Management
@tanstack/react-query^5.51.23Server state management and cachingTanStack Query Docs
jotai^2.9.3Atomic state management for client stateJotai Docs
Form Handling
react-hook-form^7.52.1Form state management and validationReact Hook Form Docs
zod^3.23.8TypeScript-first schema validationZod Docs
Styling
tailwindcss^3.4.10Utility-first CSS frameworkTailwind CSS Docs
@radix-ui/react-*^1.1.0Headless UI component primitivesRadix UI Docs
Internationalization
@lingui/core^4.11.2Core i18n functionality with macro supportLingui Docs
@lingui/react^4.11.2React integration for LinguiLingui React Docs
@lingui/macro^4.11.2Compile-time i18n message extractionLingui Macro Docs
Date Handling
date-fns^3.6.0Modern date utility library with locale supportdate-fns Docs
Utilities
clsx^2.1.1Utility for conditional CSS class namesclsx GitHub
sonner^1.5.0Toast notification systemSonner Docs
lucide-react^0.424.0Feather-inspired icon libraryLucide Icons

Development Dependencies

PackageVersionPurposeDocumentation
Testing
vitest^1.6.0Fast unit testing frameworkVitest Docs
@testing-library/react^16.0.0React component testing utilitiesTesting Library Docs
@testing-library/jest-dom^6.4.8Custom Jest matchers for DOM testingjest-dom GitHub
@playwright/test^1.46.1End-to-end testing frameworkPlaywright Docs
Code Quality
eslint^8.57.0JavaScript/TypeScript lintingESLint Docs
prettier^3.3.3Code formattingPrettier Docs
@typescript-eslint/parser^7.18.0TypeScript parser for ESLintTypeScript ESLint Docs

Internal Packages

The module also leverages internal Nimbly packages for shared functionality:

PackagePurposeRepository
@nimbly-technologies/nimbly-commonShared enums, types, and utilitiesInternal
@nimbly-technologies/ui-componentsShared UI component libraryInternal
@nimbly-technologies/api-clientStandardized API clientInternal

Package Installation & Management

Installation Commands

# Install all dependencies
yarn install
 
# Add new dependency
yarn add package-name
 
# Add development dependency  
yarn add -D package-name
 
# Update dependencies
yarn upgrade
 
# Check for outdated packages
yarn outdated

Package Resolution Configuration

File: package.json

{
  "resolutions": {
    "@types/react": "^18.2.0",
    "@types/react-dom": "^18.2.0"
  },
  "engines": {
    "node": ">=18.0.0",
    "yarn": ">=1.22.0"
  }
}

Performance Considerations

Bundle Size Optimization

The module implements several strategies to minimize bundle size:

  1. Tree Shaking: Only imports used functions from utility libraries
  2. Code Splitting: Lazy loads components using Next.js dynamic imports
  3. Minimal Dependencies: Avoids heavy libraries in favor of targeted solutions
// Tree shaking example
import { format } from 'date-fns'; // ✅ Only imports format function
import * as dateFns from 'date-fns'; // ❌ Imports entire library
 
// Code splitting example  
const BulkScheduleWizard = dynamic(() => import('./bulk-schedule-wizard'), {
  loading: () => <BulkScheduleLoadingSkeleton />,
  ssr: false,
});

Dependency Security

Regular dependency updates ensure security patches:

# Audit dependencies for vulnerabilities
yarn audit
 
# Automatically fix issues
yarn audit fix
 
# Check for high-severity issues only
yarn audit --level high

The comprehensive package ecosystem provides the bulk-schedule module with robust functionality while maintaining performance and security standards through careful dependency management and optimization strategies.


File Structure Reference

Complete Module Structure

The bulk-schedule module follows a feature-first architecture with clear separation between domain logic, UI components, and infrastructure concerns:

src/app/admin/bulk-schedule/
├── layout.tsx                          # Tab navigation layout
├── page.tsx                           # Root redirect page
├── _domain/                           # Domain entities and DTOs
│   ├── schedule-entities.ts           # Core business entities
│   ├── bulk-schedule-dtos.ts          # API request/response types
│   ├── draft-schedule-entities.ts     # Draft-specific entities
│   └── draft-schedule-dtos.ts         # Draft API types
├── _lib/                              # Business logic and utilities
│   ├── bulk-schedule-service.ts       # Core business logic
│   ├── bulk-schedule-repository.ts    # Data access layer
│   ├── bulk-schedule-queries.ts       # TanStack Query options
│   ├── bulk-schedule-hooks.ts         # Jotai-based state hooks
│   ├── bulk-schedule-atoms.ts         # Jotai atom definitions
│   ├── bulk-schedule-utils.ts         # Utility functions
│   ├── draft-schedule-service.ts      # Draft business logic
│   ├── draft-schedule-repository.ts   # Draft data access
│   ├── draft-schedule-queries.ts      # Draft query options
│   ├── draft-schedule-transformer.ts  # Data transformation
│   ├── use-bulk-schedule-navigation.ts # Navigation utilities
│   ├── use-reset-bulk-schedule.ts     # State reset utilities
│   └── use-schedule-data-cleaning.ts  # Data cleaning utilities
├── create/                            # Bulk schedule creation wizard
│   ├── page.tsx                       # Wizard entry point
│   ├── loading.tsx                    # Loading state
│   └── _components/                   # Wizard components
│       ├── bulk-schedule-wizard.tsx   # Main wizard orchestrator
│       ├── configure-schedule.tsx     # Step 1: Configuration
│       ├── global-advanced-settings.tsx # Step 2: Advanced settings
│       ├── site-department-assignment.tsx # Step 3: Site assignment
│       ├── schedule-preview.tsx       # Step 4: Preview
│       ├── questionnaire-selection.tsx # Questionnaire picker
│       ├── save-draft-modal.tsx       # Draft saving modal
│       └── wizard/                    # Wizard infrastructure
│           ├── wizard-header.tsx      # Step progress display
│           ├── wizard-step-indicator.tsx # Visual step navigation
│           ├── wizard-navigation.tsx  # Navigation controls
│           ├── use-wizard-state.ts    # Wizard state management
│           └── wizard-constants.ts    # Constants and configuration
├── drafts/                            # Draft management
│   ├── page.tsx                       # Draft list page
│   ├── _components/                   # Draft components
│   │   ├── draft-schedule-table.tsx   # Desktop table view
│   │   ├── draft-schedule-card.tsx    # Mobile card view
│   │   ├── draft-actions.tsx          # Draft action buttons
│   │   ├── draft-detail-modal.tsx     # Draft details modal
│   │   ├── search-filters.tsx         # Search and filtering
│   │   ├── draft-pagination.tsx       # Pagination component
│   │   └── draft-skeleton.tsx         # Loading skeleton
│   └── _lib/                          # Draft-specific utilities
│       ├── draft-schedule-hooks.ts    # Draft state hooks
│       └── use-questionnaire-name.ts  # Questionnaire name resolver
└── _components/                       # Shared module components
    ├── schedule-conflict-display.tsx  # Conflict visualization
    ├── schedule-preview-table.tsx     # Schedule preview table
    ├── time-range-display.tsx         # Time formatting
    ├── loading-states.tsx             # Loading components
    └── validation-indicators.tsx      # Validation feedback

Route Structure

The module defines the following application routes:

RoutePurposeComponentFeatures
/admin/bulk-scheduleModule rootRedirect to draftsTab navigation
/admin/bulk-schedule/createNew schedule wizardBulkScheduleWizardMulti-step creation
/admin/bulk-schedule/create?draft=:idEdit existing draftBulkScheduleWizardDraft loading
/admin/bulk-schedule/draftsDraft managementDraftSchedulePageList, search, filter

Documentation Summary

This comprehensive documentation covers the bulk-schedule module with over 154,000 characters of detailed technical information, including:

  • Architecture & Overview: System design, technology stack, and core principles
  • Core Domain Models: Business entities, DTOs, and type definitions
  • Business Logic & Services: Service layer implementation and business rules
  • Data Access Layer: Repository patterns, API integration, and query management
  • User Interface Components: React component architecture and state management
  • Features Deep Dive: Detailed implementation of wizard workflow and draft system
  • Integration & APIs: Complete API endpoint documentation and error handling
  • Internationalization: Multi-language support and localization patterns

The bulk-schedule module represents a comprehensive implementation of modern web development practices, providing a robust, scalable, and maintainable solution for complex scheduling requirements in enterprise applications.