Bulk Schedule Module - Technical Documentation
Table of Contents
- Architecture & Overview
- Core Domain Models
- Business Logic & Services
- Data Access Layer
- User Interface Components
- Features Deep Dive
- Integration & APIs
- Internationalization & Localization
- Package Dependencies
- 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:
- Bulk Schedule Creation - A multi-step wizard for creating schedules across multiple site-department combinations
- 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
Retrieval and Search
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:
- Try-Catch Blocks: Wrap all async operations
- Error Classification: Categorize errors by business impact
- Context Capture: Record operation parameters for debugging
- Localized Messages: Provide user-friendly error messages
- 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:
| Endpoint | Method | Purpose | Repository Method |
|---|---|---|---|
/questionnaires/questionnaireIndexes/minified | GET | Get available questionnaires | getQuestionnaires() |
/sites/compact | GET | Get sites for scheduling | getSitesForScheduling() |
/departments | GET | Get all departments | getDepartments() |
/users | GET | Get users for auditor selection | getUsersForAuditors() |
/schedules/check-conflicts | POST | Check for schedule conflicts | checkScheduleConflicts() |
/schedules/bulk/preview | POST | Generate schedule preview | generatePreview() |
/v1.0/schedules/sites/v2/bulk | PUT | Create bulk schedules | createBulkSchedules() |
/schedules/bulk/validate | POST | Validate bulk configuration | validateBulkSchedule() |
/schedules/bulk/templates | GET | Get schedule templates | getBulkScheduleTemplates() |
/schedules/bulk/templates | POST | Save schedule template | saveBulkScheduleTemplate() |
/schedules/bulk/templates/{id} | DELETE | Delete schedule template | deleteBulkScheduleTemplate() |
/v1.0/schedules/sites/draft | GET | Get draft schedules | getDraftSchedules() |
/v1.0/schedules/sites/draft | POST | Save draft schedule | saveDraftSchedule() |
/v1.0/schedules/sites/draft/{id} | PUT | Update draft schedule | updateDraftSchedule() |
/v1.0/schedules/sites/draft/{id} | DELETE | Delete draft schedule | deleteDraftSchedule() |
/v1.0/schedules/sites/draft/{id}/publish | POST | Publish draft schedule | publishDraftSchedule() |
Repository Pattern Benefits
The repository pattern provides several architectural advantages:
- Abstraction: Services work with clean interfaces rather than HTTP details
- Testability: Easy to mock repositories for unit testing
- Consistency: Standardized error handling and monitoring across all data access
- Caching: Centralized caching strategy through TanStack Query integration
- Type Safety: Full TypeScript typing from request to response
- Monitoring: Built-in performance and error tracking
- 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 namesdepartmentIds: Comma-separated department IDs for filteringincludeDisabled: Include inactive siteslimit: Maximum number of resultsoffset: 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 accessdepartmentIds[]: Filter by department accessrole: Filter by user rolesearch: 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 pagesearch: Text search across draft namessortBy: 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
| Endpoint | Method | Purpose | Repository Method | File Link |
|---|---|---|---|---|
/questionnaires/questionnaireIndexes/minified | GET | Get available questionnaires | getQuestionnaires() | bulk-schedule-repository.ts:52 |
/sites/compact | GET | Get sites for scheduling | getSitesForScheduling() | bulk-schedule-repository.ts:78 |
/departments | GET | Get all departments | getDepartments() | bulk-schedule-repository.ts:115 |
/users | GET | Get users for auditor selection | getUsersForAuditors() | bulk-schedule-repository.ts:130 |
/schedules/check-conflicts | POST | Check for schedule conflicts | checkScheduleConflicts() | bulk-schedule-repository.ts:171 |
/schedules/bulk/preview | POST | Generate schedule preview | generatePreview() | bulk-schedule-repository.ts:190 |
/v1.0/schedules/sites/v2/bulk | PUT | Create bulk schedules | createBulkSchedules() | bulk-schedule-repository.ts:220 |
/schedules/bulk/validate | POST | Validate bulk configuration | validateBulkSchedule() | bulk-schedule-repository.ts:259 |
/schedules/bulk/templates | GET | Get schedule templates | getBulkScheduleTemplates() | bulk-schedule-repository.ts:285 |
/schedules/bulk/templates | POST | Save schedule template | saveBulkScheduleTemplate() | bulk-schedule-repository.ts:320 |
/schedules/bulk/templates/{id} | DELETE | Delete schedule template | deleteBulkScheduleTemplate() | bulk-schedule-repository.ts:355 |
/v1.0/schedules/sites/draft | GET | Get draft schedules | getDraftSchedules() | draft-schedule-repository.ts:25 |
/v1.0/schedules/sites/draft | POST | Save draft schedule | saveDraftSchedule() | draft-schedule-repository.ts:75 |
/v1.0/schedules/sites/draft/{id} | GET | Get draft by ID | getDraftScheduleById() | draft-schedule-repository.ts:55 |
/v1.0/schedules/sites/draft/{id} | PUT | Update draft schedule | updateDraftSchedule() | draft-schedule-repository.ts:90 |
/v1.0/schedules/sites/draft/{id} | DELETE | Delete draft schedule | deleteDraftSchedule() | draft-schedule-repository.ts:105 |
/v1.0/schedules/sites/draft/{id}/publish | POST | Publish draft schedule | publishDraftSchedule() | 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:
| Language | Code | Region | File Path | Status |
|---|---|---|---|---|
| English | en | United States | src/locales/en.po | Complete |
| Spanish | es | Latin America | src/locales/es.po | Complete |
| Portuguese | pt | Brazil | src/locales/pt.po | Complete |
| Indonesian | id | Indonesia | src/locales/id.po | Complete |
| Korean | ko | South Korea | src/locales/ko.po | Complete |
| Thai | th | Thailand | src/locales/th.po | Complete |
| Chinese (Mandarin) | cmn | China/Taiwan | src/locales/cmn.po | Complete |
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:
| Package | Version | Purpose | Documentation |
|---|---|---|---|
| React Framework | |||
react | ^18.2.0 | Core UI framework | React Docs |
next | ^15.0.0 | Full-stack React framework with App Router | Next.js Docs |
| TypeScript | |||
typescript | ^5.3.0 | Type safety and developer experience | TypeScript Docs |
| State Management | |||
@tanstack/react-query | ^5.51.23 | Server state management and caching | TanStack Query Docs |
jotai | ^2.9.3 | Atomic state management for client state | Jotai Docs |
| Form Handling | |||
react-hook-form | ^7.52.1 | Form state management and validation | React Hook Form Docs |
zod | ^3.23.8 | TypeScript-first schema validation | Zod Docs |
| Styling | |||
tailwindcss | ^3.4.10 | Utility-first CSS framework | Tailwind CSS Docs |
@radix-ui/react-* | ^1.1.0 | Headless UI component primitives | Radix UI Docs |
| Internationalization | |||
@lingui/core | ^4.11.2 | Core i18n functionality with macro support | Lingui Docs |
@lingui/react | ^4.11.2 | React integration for Lingui | Lingui React Docs |
@lingui/macro | ^4.11.2 | Compile-time i18n message extraction | Lingui Macro Docs |
| Date Handling | |||
date-fns | ^3.6.0 | Modern date utility library with locale support | date-fns Docs |
| Utilities | |||
clsx | ^2.1.1 | Utility for conditional CSS class names | clsx GitHub |
sonner | ^1.5.0 | Toast notification system | Sonner Docs |
lucide-react | ^0.424.0 | Feather-inspired icon library | Lucide Icons |
Development Dependencies
| Package | Version | Purpose | Documentation |
|---|---|---|---|
| Testing | |||
vitest | ^1.6.0 | Fast unit testing framework | Vitest Docs |
@testing-library/react | ^16.0.0 | React component testing utilities | Testing Library Docs |
@testing-library/jest-dom | ^6.4.8 | Custom Jest matchers for DOM testing | jest-dom GitHub |
@playwright/test | ^1.46.1 | End-to-end testing framework | Playwright Docs |
| Code Quality | |||
eslint | ^8.57.0 | JavaScript/TypeScript linting | ESLint Docs |
prettier | ^3.3.3 | Code formatting | Prettier Docs |
@typescript-eslint/parser | ^7.18.0 | TypeScript parser for ESLint | TypeScript ESLint Docs |
Internal Packages
The module also leverages internal Nimbly packages for shared functionality:
| Package | Purpose | Repository |
|---|---|---|
@nimbly-technologies/nimbly-common | Shared enums, types, and utilities | Internal |
@nimbly-technologies/ui-components | Shared UI component library | Internal |
@nimbly-technologies/api-client | Standardized API client | Internal |
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 outdatedPackage 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:
- Tree Shaking: Only imports used functions from utility libraries
- Code Splitting: Lazy loads components using Next.js dynamic imports
- 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 highThe 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:
| Route | Purpose | Component | Features |
|---|---|---|---|
/admin/bulk-schedule | Module root | Redirect to drafts | Tab navigation |
/admin/bulk-schedule/create | New schedule wizard | BulkScheduleWizard | Multi-step creation |
/admin/bulk-schedule/create?draft=:id | Edit existing draft | BulkScheduleWizard | Draft loading |
/admin/bulk-schedule/drafts | Draft management | DraftSchedulePage | List, 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.
Related Features
- Bulk Schedule Overview - User-facing documentation and workflows
- Schedule Management - Core scheduling functionality
- Site Management - Site and department configuration
- Questionnaire System - Audit form management
- User Management - User roles and permissions
- Notification System - Alert and reminder configuration
- Department Management - Organizational unit setup
- Issue Management - Issue tracking and assignment
- Ad Hoc Schedules - On-demand inspection capabilities
- Next.js Architecture - Framework implementation patterns
- Internationalization - Multi-language support
- Admin Application - Platform overview and features