# Solution Design Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] All context sources are listed with relevance ratings
- [x] Project commands are discovered from actual project files
- [x] Constraints → Strategy → Design → Implementation path is logical
- [x] Architecture pattern is clearly stated with rationale
- [x] Every component in diagram has directory mapping
- [x] Every interface has specification
- [x] Error handling covers all error types
- [x] Quality requirements are specific and measurable
- [x] Every quality requirement has test coverage
- [x] **All architecture decisions confirmed by user** ✅
- [x] Component names consistent across diagrams
- [x] A developer could implement from this design

---

## Constraints

**CON-1 Framework & Language Requirements**
- Flutter 3.38.4 / Dart 3.10.3
- iOS 12+ and Android API 24+ minimum targets
- Must follow Clean Architecture with Riverpod 3.x (Notifier pattern)
- Data models must use Freezed 3.x with `abstract class` keyword
- Domain entities must use Equatable for equality

**CON-2 Coding Standards & Patterns**
- State machines must use sealed classes extending Equatable
- Repository pattern: domain interfaces in `lib/domain/repositories/`, implementations in `lib/data/repositories/`
- All providers must be NotifierProvider (not StateNotifier - deprecated in Riverpod 3.x)
- API calls through Dio client with existing interceptors
- Error handling via custom AppException hierarchy

**CON-3 Backend & Integration Requirements**
- Backend APIs follow existing REST patterns from buyerkiosk-web
- Push notifications via existing FCM infrastructure
- Store-scoped endpoints using `typeNum` parameter
- Authentication via existing JWT token flow with refresh
- Manager approval UI is web-only (not in scope for mobile)

**CON-4 Performance & UX Requirements**
- Optimistic UI updates for request submissions
- Offline-capable: queue requests when offline, retry on reconnect
- Deep linking from push notifications to relevant screens
- Pull-to-refresh on all list screens

## Implementation Context

### Required Context Sources

```yaml
# Internal documentation and patterns
- doc: CLAUDE.md
  relevance: CRITICAL
  why: "Project-wide patterns, tech stack, and architecture guidelines"

- doc: docs/specs/007-employee-shift-requests/product-requirements.md
  relevance: CRITICAL
  why: "Complete PRD with state machines, eligibility rules, and acceptance criteria"

# Existing codebase patterns (HIGH relevance)
- file: lib/presentation/providers/auth_provider.dart
  relevance: HIGH
  why: "Reference for Notifier pattern, state transitions, error handling"

- file: lib/presentation/providers/notification_provider.dart
  relevance: HIGH
  why: "Notification integration pattern for push notification handling"

- file: lib/presentation/providers/avatar_provider.dart
  relevance: HIGH
  why: "State machine pattern with upload progress and retry"

- file: lib/domain/entities/auth_state.dart
  relevance: HIGH
  why: "Sealed class pattern for state machine implementation"

- file: lib/domain/entities/notification_state.dart
  relevance: HIGH
  why: "Notification state pattern to extend for request notifications"

- file: lib/data/repositories/notification_repository_impl.dart
  relevance: HIGH
  why: "Repository implementation pattern with error handling"

- file: lib/data/models/open_shift_model.dart
  relevance: HIGH
  why: "Freezed model pattern for shift-related data"

- file: lib/domain/entities/open_shift.dart
  relevance: HIGH
  why: "Equatable entity pattern with helper methods"

# Existing infrastructure (MEDIUM relevance)
- file: lib/core/network/api_client.dart
  relevance: MEDIUM
  why: "HTTP client patterns and error handling"

- file: lib/core/constants/api_constants.dart
  relevance: MEDIUM
  why: "Endpoint definition patterns"

- file: lib/router/app_router.dart
  relevance: MEDIUM
  why: "Navigation and deep linking patterns"

- file: lib/core/services/push_notification_service.dart
  relevance: MEDIUM
  why: "Push notification service for request status updates"

- file: lib/core/constants/notification_constants.dart
  relevance: MEDIUM
  why: "Notification type constants to extend"
```

### Implementation Boundaries

- **Must Preserve**:
  - Existing auth flow and token management
  - Existing notification infrastructure
  - Existing schedule viewing functionality
  - Store context and selection flow

- **Can Modify**:
  - Add new notification types to `NotificationConstants`
  - Add new routes to `app_router.dart`
  - Extend `api_constants.dart` with new endpoints
  - Add new navigation items to home/settings screens

- **Must Not Touch**:
  - Backend API implementation (separate codebase)
  - Manager approval flow (web-only)
  - Existing clock-in/out functionality
  - Biometric authentication flow

### External Interfaces

#### System Context Diagram

```mermaid
graph TB
    subgraph "Mobile App"
        UI[Shift Request UI]
        Provider[ShiftRequestsProvider]
        Repo[ShiftRequestsRepository]
    end

    User[Team Member] --> UI
    UI --> Provider
    Provider --> Repo

    Repo --> API[Backend REST API]
    API --> DB[(Database)]

    FCM[Firebase Cloud Messaging] --> UI
    API --> FCM

    Coworker[Coworker] --> FCM
    Manager[Manager - Web] --> API
```

#### Interface Specifications

```yaml
# Inbound Interfaces
inbound:
  - name: "Push Notifications"
    type: FCM
    format: JSON payload
    authentication: FCM token
    data_flow: "Request status updates, swap requests from coworkers"
    notification_types:
      - time_off_submitted
      - time_off_approved
      - time_off_denied
      - swap_request_received
      - swap_accepted
      - swap_declined
      - swap_approved
      - swap_denied
      - swap_expired

# Outbound Interfaces (API calls)
outbound:
  - name: "Time-Off Request API"
    type: HTTPS
    format: REST/JSON
    authentication: JWT Bearer token
    base_path: /api/mobile/scheduling/:typeNum/requests/time-off
    endpoints:
      - GET / (list requests)
      - POST / (submit request)
      - DELETE /:requestId (cancel request)

  - name: "Shift Swap Request API"
    type: HTTPS
    format: REST/JSON
    authentication: JWT Bearer token
    base_path: /api/mobile/scheduling/:typeNum/requests/swap
    endpoints:
      - GET / (list swap requests)
      - POST / (initiate swap)
      - PUT /:requestId/respond (accept/decline)
      - DELETE /:requestId (cancel/withdraw)

  - name: "Team Schedule API"
    type: HTTPS
    format: REST/JSON
    authentication: JWT Bearer token
    base_path: /api/mobile/scheduling/:typeNum/team-schedule
    endpoints:
      - GET / (list team shifts for date range)
```

### Project Commands

```bash
# Environment Setup
Install Dependencies: flutter pub get
Regenerate Freezed: dart run build_runner build --delete-conflicting-outputs
Start Development: flutter run

# Testing Commands
Unit Tests: flutter test
Widget Tests: flutter test test/presentation/
Integration Tests: flutter test test/integration/
Test Coverage: flutter test --coverage

# Code Quality Commands
Linting: flutter analyze
Formatting: dart format lib/ test/

# Build Commands
Build iOS (debug): flutter build ios --debug --no-codesign
Build Android (debug): flutter build apk --debug
```

## Solution Strategy

### Architecture Pattern: Clean Architecture with Riverpod

Following the existing codebase pattern, this feature implements **Clean Architecture** with three distinct layers:

1. **Domain Layer** (inner): Entities, repository interfaces, business rules
2. **Data Layer** (middle): Models, repository implementations, API mappers
3. **Presentation Layer** (outer): Providers (Notifiers), screens, widgets

**Integration Approach:**
- New feature adds to existing layer structure, no architectural changes
- Extends existing patterns (Notifier, sealed classes, Freezed models)
- Reuses existing infrastructure (ApiClient, PushNotificationService, StorageService)
- New navigation routes integrated into existing GoRouter configuration

**Justification:**
- Consistent with existing codebase patterns
- Testable: each layer can be tested independently
- Maintainable: clear separation of concerns
- Scalable: new features follow same patterns

**Key Decisions:**
1. **State Machine per Request Type**: Separate sealed classes for TimeOffRequest and ShiftSwapRequest states (matches PRD)
2. **Single Provider with Derived Providers**: One main `shiftRequestsProvider` with derived providers for filtered views
3. **Optimistic Updates**: Submit requests immediately show in UI, revert on error
4. **Notification-Driven Sync**: Real-time updates via push notifications, not polling

## Building Block View

### Components

```mermaid
graph TB
    subgraph Presentation["Presentation Layer"]
        RequestsScreen[RequestsScreen]
        TimeOffForm[TimeOffRequestScreen]
        SwapForm[SwapRequestScreen]
        TeamSchedule[TeamScheduleScreen]
        RequestDetail[RequestDetailScreen]

        ShiftRequestsProvider[ShiftRequestsProvider]
        TeamScheduleProvider[TeamScheduleProvider]

        RequestCard[RequestCard Widget]
        SwapPreview[SwapPreview Widget]
    end

    subgraph Domain["Domain Layer"]
        TimeOffRequest[TimeOffRequest Entity]
        ShiftSwapRequest[ShiftSwapRequest Entity]
        TeamShift[TeamShift Entity]
        RequestState[Request State Machines]

        ShiftRequestsRepo[ShiftRequestsRepository Interface]
        TeamScheduleRepo[TeamScheduleRepository Interface]
    end

    subgraph Data["Data Layer"]
        TimeOffModel[TimeOffRequestModel]
        SwapModel[ShiftSwapRequestModel]
        TeamShiftModel[TeamShiftModel]

        ShiftRequestsRepoImpl[ShiftRequestsRepositoryImpl]
        TeamScheduleRepoImpl[TeamScheduleRepositoryImpl]
    end

    subgraph Core["Core Services"]
        ApiClient[ApiClient]
        PushService[PushNotificationService]
        Storage[StorageService]
    end

    RequestsScreen --> ShiftRequestsProvider
    TimeOffForm --> ShiftRequestsProvider
    SwapForm --> ShiftRequestsProvider
    TeamSchedule --> TeamScheduleProvider

    ShiftRequestsProvider --> ShiftRequestsRepo
    TeamScheduleProvider --> TeamScheduleRepo

    ShiftRequestsRepoImpl -.implements.-> ShiftRequestsRepo
    TeamScheduleRepoImpl -.implements.-> TeamScheduleRepo

    ShiftRequestsRepoImpl --> ApiClient
    TeamScheduleRepoImpl --> ApiClient

    ShiftRequestsRepoImpl --> TimeOffModel
    ShiftRequestsRepoImpl --> SwapModel
    TeamScheduleRepoImpl --> TeamShiftModel

    TimeOffModel -.maps to.-> TimeOffRequest
    SwapModel -.maps to.-> ShiftSwapRequest
    TeamShiftModel -.maps to.-> TeamShift

    PushService --> ShiftRequestsProvider
```

### Directory Map

```
lib/
├── core/
│   └── constants/
│       └── shift_request_constants.dart          # NEW: Request-related constants
│
├── domain/
│   ├── entities/
│   │   ├── time_off_request.dart                 # NEW: TimeOffRequest entity
│   │   ├── shift_swap_request.dart               # NEW: ShiftSwapRequest entity
│   │   ├── team_shift.dart                       # NEW: TeamShift entity (for swap selection)
│   │   └── shift_request_state.dart              # NEW: Sealed state classes
│   └── repositories/
│       ├── shift_requests_repository.dart        # NEW: Request operations interface
│       └── team_schedule_repository.dart         # NEW: Team schedule interface
│
├── data/
│   ├── models/
│   │   ├── time_off_request_model.dart           # NEW: Freezed model
│   │   ├── time_off_request_model.freezed.dart   # GENERATED
│   │   ├── time_off_request_model.g.dart         # GENERATED
│   │   ├── shift_swap_request_model.dart         # NEW: Freezed model
│   │   ├── shift_swap_request_model.freezed.dart # GENERATED
│   │   ├── shift_swap_request_model.g.dart       # GENERATED
│   │   ├── team_shift_model.dart                 # NEW: Freezed model
│   │   ├── team_shift_model.freezed.dart         # GENERATED
│   │   └── team_shift_model.g.dart               # GENERATED
│   └── repositories/
│       ├── shift_requests_repository_impl.dart   # NEW: API implementation
│       └── team_schedule_repository_impl.dart    # NEW: API implementation
│
├── presentation/
│   ├── providers/
│   │   ├── shift_requests_provider.dart          # NEW: Main request state management
│   │   └── team_schedule_provider.dart           # NEW: Team schedule for swaps
│   ├── screens/
│   │   └── requests/
│   │       ├── requests_screen.dart              # NEW: Request list/tabs
│   │       ├── time_off_request_screen.dart      # NEW: Time-off form
│   │       ├── swap_request_screen.dart          # NEW: Swap initiation form
│   │       ├── team_schedule_screen.dart         # NEW: Team schedule browser
│   │       └── request_detail_screen.dart        # NEW: Request details
│   └── widgets/
│       └── requests/
│           ├── request_card.dart                 # NEW: Request list item
│           ├── request_status_badge.dart         # NEW: Status indicator
│           ├── swap_preview_card.dart            # NEW: Swap comparison view
│           └── team_shift_card.dart              # NEW: Team schedule shift item
│
├── router/
│   └── app_router.dart                           # MODIFY: Add request routes
│
└── core/
    ├── constants/
    │   ├── api_constants.dart                    # MODIFY: Add request endpoints
    │   ├── notification_constants.dart           # MODIFY: Add request notification types
    │   └── shift_request_constants.dart          # NEW: Request-specific constants
    └── services/
        ├── notification_navigation_service.dart  # MODIFY: Add request deep links
        └── offline_queue_service.dart            # NEW: Offline request queue

test/
├── domain/
│   └── entities/
│       ├── time_off_request_test.dart            # NEW
│       └── shift_swap_request_test.dart          # NEW
├── data/
│   └── repositories/
│       ├── shift_requests_repository_test.dart   # NEW
│       └── team_schedule_repository_test.dart    # NEW
├── presentation/
│   ├── providers/
│   │   ├── shift_requests_provider_test.dart     # NEW
│   │   └── team_schedule_provider_test.dart      # NEW
│   └── screens/
│       └── requests/
│           ├── requests_screen_test.dart         # NEW
│           └── time_off_request_screen_test.dart # NEW
└── integration/
    └── shift_requests_flow_test.dart             # NEW
```

### Interface Specifications

#### Data Models (Freezed)

```dart
// lib/data/models/time_off_request_model.dart
@freezed
abstract class TimeOffRequestModel with _$TimeOffRequestModel {
  const factory TimeOffRequestModel({
    required int requestId,
    required String startDate,          // ISO 8601 date
    required String endDate,            // ISO 8601 date
    String? reason,
    required String status,             // pending, approved, denied, cancelled
    String? denialReason,
    required String submittedAt,        // ISO 8601 datetime
    String? reviewedAt,
    String? reviewedByName,
  }) = _TimeOffRequestModel;

  factory TimeOffRequestModel.fromJson(Map<String, dynamic> json) =>
      _$TimeOffRequestModelFromJson(json);
}

// lib/data/models/shift_swap_request_model.dart
@freezed
abstract class ShiftSwapRequestModel with _$ShiftSwapRequestModel {
  const factory ShiftSwapRequestModel({
    required int requestId,
    required ShiftSummaryModel requestorShift,
    required ShiftSummaryModel targetShift,
    required EmployeeSummaryModel requestor,
    required EmployeeSummaryModel target,
    String? message,
    required String status,             // pending_coworker, declined, pending_manager, approved, denied, expired, cancelled, invalidated
    String? declineReason,
    String? denialReason,
    required String createdAt,
    String? coworkerResponseAt,
    String? managerDecisionAt,
    String? expiresAt,
  }) = _ShiftSwapRequestModel;

  factory ShiftSwapRequestModel.fromJson(Map<String, dynamic> json) =>
      _$ShiftSwapRequestModelFromJson(json);
}

@freezed
abstract class ShiftSummaryModel with _$ShiftSummaryModel {
  const factory ShiftSummaryModel({
    required int shiftId,
    required String date,
    required String startTime,
    required String endTime,
    required double durationHours,
    String? positionName,
  }) = _ShiftSummaryModel;

  factory ShiftSummaryModel.fromJson(Map<String, dynamic> json) =>
      _$ShiftSummaryModelFromJson(json);
}

@freezed
abstract class EmployeeSummaryModel with _$EmployeeSummaryModel {
  const factory EmployeeSummaryModel({
    required String odooUserId,
    required String firstName,
    String? photoUrl,
  }) = _EmployeeSummaryModel;

  factory EmployeeSummaryModel.fromJson(Map<String, dynamic> json) =>
      _$EmployeeSummaryModelFromJson(json);
}

// lib/data/models/team_shift_model.dart
@freezed
abstract class TeamShiftModel with _$TeamShiftModel {
  const factory TeamShiftModel({
    required int shiftId,
    required String date,
    required String startTime,
    required String endTime,
    required double durationHours,
    required String employeeFirstName,
    String? employeePhotoUrl,
    required String positionName,
    required int positionId,
    required bool isEligibleForSwap,      // Pre-calculated by backend
    String? ineligibilityReason,          // Why not eligible (if applicable)
  }) = _TeamShiftModel;

  factory TeamShiftModel.fromJson(Map<String, dynamic> json) =>
      _$TeamShiftModelFromJson(json);
}
```

#### Domain Entities (Equatable)

```dart
// lib/domain/entities/time_off_request.dart
class TimeOffRequest extends Equatable {
  final int requestId;
  final DateTime startDate;
  final DateTime endDate;
  final String? reason;
  final TimeOffRequestStatus status;
  final String? denialReason;
  final DateTime submittedAt;
  final DateTime? reviewedAt;
  final String? reviewedByName;

  const TimeOffRequest({
    required this.requestId,
    required this.startDate,
    required this.endDate,
    this.reason,
    required this.status,
    this.denialReason,
    required this.submittedAt,
    this.reviewedAt,
    this.reviewedByName,
  });

  // Status helpers
  bool get isPending => status == TimeOffRequestStatus.pending;
  bool get isApproved => status == TimeOffRequestStatus.approved;
  bool get isDenied => status == TimeOffRequestStatus.denied;
  bool get isCancelled => status == TimeOffRequestStatus.cancelled;
  bool get canCancel => isPending;

  // Display helpers
  String get dateRangeDisplay {
    if (startDate.year == endDate.year &&
        startDate.month == endDate.month &&
        startDate.day == endDate.day) {
      return _formatDate(startDate);
    }
    return '${_formatDate(startDate)} - ${_formatDate(endDate)}';
  }

  int get totalDays => endDate.difference(startDate).inDays + 1;

  String _formatDate(DateTime date) {
    const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    return '${months[date.month - 1]} ${date.day}';
  }

  @override
  List<Object?> get props => [
    requestId, startDate, endDate, reason, status,
    denialReason, submittedAt, reviewedAt, reviewedByName,
  ];
}

enum TimeOffRequestStatus { pending, approved, denied, cancelled }

// lib/domain/entities/shift_swap_request.dart
class ShiftSwapRequest extends Equatable {
  final int requestId;
  final ShiftSummary requestorShift;
  final ShiftSummary targetShift;
  final EmployeeSummary requestor;
  final EmployeeSummary target;
  final String? message;
  final ShiftSwapStatus status;
  final String? declineReason;
  final String? denialReason;
  final DateTime createdAt;
  final DateTime? coworkerResponseAt;
  final DateTime? managerDecisionAt;
  final DateTime? expiresAt;

  const ShiftSwapRequest({
    required this.requestId,
    required this.requestorShift,
    required this.targetShift,
    required this.requestor,
    required this.target,
    this.message,
    required this.status,
    this.declineReason,
    this.denialReason,
    required this.createdAt,
    this.coworkerResponseAt,
    this.managerDecisionAt,
    this.expiresAt,
  });

  // Status helpers
  bool get isPendingCoworker => status == ShiftSwapStatus.pendingCoworker;
  bool get isPendingManager => status == ShiftSwapStatus.pendingManager;
  bool get isDeclined => status == ShiftSwapStatus.declined;
  bool get isApproved => status == ShiftSwapStatus.approved;
  bool get isDenied => status == ShiftSwapStatus.denied;
  bool get isExpired => status == ShiftSwapStatus.expired;
  bool get isCancelled => status == ShiftSwapStatus.cancelled;
  bool get isInvalidated => status == ShiftSwapStatus.invalidated;

  bool get isPending => isPendingCoworker || isPendingManager;
  bool get isTerminal => !isPending;
  bool get canCancel => isPending;

  // Who needs to act next
  String get awaitingAction {
    if (isPendingCoworker) return 'Awaiting ${target.firstName}';
    if (isPendingManager) return 'Awaiting Manager';
    return '';
  }

  @override
  List<Object?> get props => [
    requestId, requestorShift, targetShift, requestor, target,
    message, status, declineReason, denialReason, createdAt,
    coworkerResponseAt, managerDecisionAt, expiresAt,
  ];
}

enum ShiftSwapStatus {
  pendingCoworker,
  declined,
  pendingManager,
  approved,
  denied,
  expired,
  cancelled,
  invalidated,
}
```

#### State Machines (Sealed Classes)

```dart
// lib/domain/entities/shift_request_state.dart

/// Main provider state for shift requests screen
sealed class ShiftRequestsState extends Equatable {
  const ShiftRequestsState();
  @override
  List<Object?> get props => [];
}

class ShiftRequestsInitial extends ShiftRequestsState {
  const ShiftRequestsInitial();
}

class ShiftRequestsLoading extends ShiftRequestsState {
  const ShiftRequestsLoading();
}

class ShiftRequestsLoaded extends ShiftRequestsState {
  final List<TimeOffRequest> timeOffRequests;
  final List<ShiftSwapRequest> swapRequests;
  final DateTime lastUpdated;

  const ShiftRequestsLoaded({
    required this.timeOffRequests,
    required this.swapRequests,
    required this.lastUpdated,
  });

  @override
  List<Object?> get props => [timeOffRequests, swapRequests, lastUpdated];
}

class ShiftRequestsError extends ShiftRequestsState {
  final String message;
  final bool canRetry;

  const ShiftRequestsError({
    required this.message,
    this.canRetry = true,
  });

  @override
  List<Object?> get props => [message, canRetry];
}

/// Form submission state for time-off requests
sealed class TimeOffSubmissionState extends Equatable {
  const TimeOffSubmissionState();
  @override
  List<Object?> get props => [];
}

class TimeOffSubmissionIdle extends TimeOffSubmissionState {
  const TimeOffSubmissionIdle();
}

class TimeOffSubmissionValidating extends TimeOffSubmissionState {
  const TimeOffSubmissionValidating();
}

class TimeOffSubmissionSubmitting extends TimeOffSubmissionState {
  const TimeOffSubmissionSubmitting();
}

class TimeOffSubmissionSuccess extends TimeOffSubmissionState {
  final TimeOffRequest request;
  const TimeOffSubmissionSuccess(this.request);
  @override
  List<Object?> get props => [request];
}

class TimeOffSubmissionError extends TimeOffSubmissionState {
  final String message;
  final TimeOffSubmissionErrorType type;

  const TimeOffSubmissionError({
    required this.message,
    required this.type,
  });

  @override
  List<Object?> get props => [message, type];
}

enum TimeOffSubmissionErrorType {
  pastDate,
  tooSoon,
  duplicate,
  network,
  server,
  unknown,
}

/// Form submission state for swap requests
sealed class SwapSubmissionState extends Equatable {
  const SwapSubmissionState();
  @override
  List<Object?> get props => [];
}

class SwapSubmissionIdle extends SwapSubmissionState {
  const SwapSubmissionIdle();
}

class SwapSubmissionSubmitting extends SwapSubmissionState {
  const SwapSubmissionSubmitting();
}

class SwapSubmissionSuccess extends SwapSubmissionState {
  final ShiftSwapRequest request;
  const SwapSubmissionSuccess(this.request);
  @override
  List<Object?> get props => [request];
}

class SwapSubmissionError extends SwapSubmissionState {
  final String message;
  final SwapSubmissionErrorType type;

  const SwapSubmissionError({
    required this.message,
    required this.type,
  });

  @override
  List<Object?> get props => [message, type];
}

enum SwapSubmissionErrorType {
  shiftTooSoon,
  alreadyPending,
  notQualified,
  network,
  server,
  unknown,
}

/// Team schedule browsing state
sealed class TeamScheduleState extends Equatable {
  const TeamScheduleState();
  @override
  List<Object?> get props => [];
}

class TeamScheduleInitial extends TeamScheduleState {
  const TeamScheduleInitial();
}

class TeamScheduleLoading extends TeamScheduleState {
  const TeamScheduleLoading();
}

class TeamScheduleLoaded extends TeamScheduleState {
  final List<TeamShift> shifts;
  final DateTime weekStart;
  final DateTime weekEnd;
  final String? positionFilter;

  const TeamScheduleLoaded({
    required this.shifts,
    required this.weekStart,
    required this.weekEnd,
    this.positionFilter,
  });

  @override
  List<Object?> get props => [shifts, weekStart, weekEnd, positionFilter];
}

class TeamScheduleError extends TeamScheduleState {
  final String message;
  const TeamScheduleError(this.message);
  @override
  List<Object?> get props => [message];
}
```

#### Repository Interfaces

```dart
// lib/domain/repositories/shift_requests_repository.dart
abstract class ShiftRequestsRepository {
  /// Get all time-off requests for current user at store
  Future<List<TimeOffRequest>> getTimeOffRequests({
    required String typeNum,
    TimeOffRequestFilter? filter,
  });

  /// Submit a new time-off request
  Future<TimeOffRequest> submitTimeOffRequest({
    required String typeNum,
    required DateTime startDate,
    required DateTime endDate,
    String? reason,
  });

  /// Cancel a pending time-off request
  Future<void> cancelTimeOffRequest({
    required String typeNum,
    required int requestId,
  });

  /// Get all swap requests (initiated by user and received)
  Future<List<ShiftSwapRequest>> getSwapRequests({
    required String typeNum,
  });

  /// Initiate a new swap request
  Future<ShiftSwapRequest> initiateSwapRequest({
    required String typeNum,
    required int myShiftId,
    required int targetShiftId,
    String? message,
  });

  /// Respond to an incoming swap request (as coworker)
  Future<ShiftSwapRequest> respondToSwapRequest({
    required String typeNum,
    required int requestId,
    required bool accepted,
    String? declineReason,
  });

  /// Cancel/withdraw a pending swap request
  Future<void> cancelSwapRequest({
    required String typeNum,
    required int requestId,
  });
}

class TimeOffRequestFilter extends Equatable {
  final TimeOffRequestStatus? status;
  final DateTime? startDate;
  final DateTime? endDate;

  const TimeOffRequestFilter({
    this.status,
    this.startDate,
    this.endDate,
  });

  @override
  List<Object?> get props => [status, startDate, endDate];
}

// lib/domain/repositories/team_schedule_repository.dart
abstract class TeamScheduleRepository {
  /// Get team schedule for swap selection
  Future<List<TeamShift>> getTeamSchedule({
    required String typeNum,
    required DateTime weekStart,
    int? positionId,        // Optional position filter
    int? excludeShiftId,    // Exclude user's own shift being swapped
  });

  /// Get available positions for filtering
  Future<List<Position>> getPositions({
    required String typeNum,
  });
}
```

#### API Endpoints (Constants)

```dart
// Additions to lib/core/constants/api_constants.dart
class ApiConstants {
  // ... existing constants ...

  // Time-Off Request endpoints
  static String timeOffRequests(String typeNum) =>
      '$apiBasePath/$typeNum/requests/time-off';
  static String timeOffRequest(String typeNum, int requestId) =>
      '$apiBasePath/$typeNum/requests/time-off/$requestId';

  // Shift Swap Request endpoints
  static String swapRequests(String typeNum) =>
      '$apiBasePath/$typeNum/requests/swap';
  static String swapRequest(String typeNum, int requestId) =>
      '$apiBasePath/$typeNum/requests/swap/$requestId';
  static String respondToSwap(String typeNum, int requestId) =>
      '$apiBasePath/$typeNum/requests/swap/$requestId/respond';

  // Team Schedule endpoints
  static String teamSchedule(String typeNum) =>
      '$apiBasePath/$typeNum/team-schedule';
  static String positions(String typeNum) =>
      '$apiBasePath/$typeNum/positions';
}
```

#### Notification Types

```dart
// Additions to lib/core/constants/notification_constants.dart
class NotificationConstants {
  // ... existing constants ...

  // Time-Off Request notification types
  static const String typeTimeOffSubmitted = 'time_off_submitted';
  static const String typeTimeOffApproved = 'time_off_approved';
  static const String typeTimeOffDenied = 'time_off_denied';

  // Shift Swap notification types
  static const String typeSwapRequestReceived = 'swap_request_received';
  static const String typeSwapAccepted = 'swap_accepted';
  static const String typeSwapDeclined = 'swap_declined';
  static const String typeSwapApproved = 'swap_approved';
  static const String typeSwapDenied = 'swap_denied';
  static const String typeSwapExpired = 'swap_expired';
  static const String typeSwapCancelled = 'swap_cancelled';
}
```

## Runtime View

### Primary Flow: Submit Time-Off Request

1. User taps "Request Time Off" from Requests screen
2. TimeOffRequestScreen displays date picker form
3. User selects start date (required) and end date (optional, defaults to same day)
4. User optionally adds reason
5. User taps "Submit Request"
6. Provider validates input (client-side)
7. Provider calls repository.submitTimeOffRequest()
8. Repository makes POST to /api/mobile/scheduling/:typeNum/requests/time-off
9. On success: Request added to list, user navigates back, sees confirmation
10. Push notification sent confirming submission
11. Later: Push notification when manager approves/denies

```mermaid
sequenceDiagram
    actor User
    participant Screen as TimeOffRequestScreen
    participant Provider as ShiftRequestsProvider
    participant Repo as ShiftRequestsRepository
    participant API as Backend API
    participant FCM as Push Notifications

    User->>Screen: Tap "Request Time Off"
    Screen->>Screen: Show date picker form
    User->>Screen: Select dates, add reason
    User->>Screen: Tap "Submit"

    Screen->>Provider: submitTimeOffRequest(dates, reason)
    Provider->>Provider: Validate dates (client-side)
    alt Validation fails
        Provider-->>Screen: TimeOffSubmissionError
        Screen-->>User: Show error message
    else Validation passes
        Provider->>Provider: state = Submitting
        Provider->>Repo: submitTimeOffRequest(...)
        Repo->>API: POST /requests/time-off
        alt Success
            API-->>Repo: 201 Created + Request
            Repo-->>Provider: TimeOffRequest
            Provider->>Provider: Add to list, state = Success
            Provider-->>Screen: TimeOffSubmissionSuccess
            Screen-->>User: Show success, navigate back
            API->>FCM: Send confirmation notification
            FCM-->>User: "Request Submitted" push
        else Error
            API-->>Repo: Error response
            Repo-->>Provider: Exception
            Provider->>Provider: state = Error
            Provider-->>Screen: TimeOffSubmissionError
            Screen-->>User: Show error, allow retry
        end
    end
```

### Secondary Flow: Initiate Shift Swap

1. User views their scheduled shift on Schedule screen
2. User taps "Find Swap" on the shift
3. System navigates to TeamScheduleScreen for that week
4. User browses team shifts, can filter by position
5. User selects a coworker's compatible shift
6. System shows SwapPreviewCard with both shifts
7. User optionally adds message, taps "Request Swap"
8. **TeamScheduleProvider** loads team schedule; **ShiftRequestsProvider** handles swap submission
9. On success: Swap request created, coworker notified
10. User sees pending swap in their requests list

```mermaid
sequenceDiagram
    actor User
    participant Schedule as ScheduleScreen
    participant TeamSched as TeamScheduleScreen
    participant TeamProvider as TeamScheduleProvider
    participant SwapProvider as ShiftRequestsProvider
    participant TeamRepo as TeamScheduleRepository
    participant SwapRepo as ShiftRequestsRepository
    participant API as Backend API
    participant FCM as Push Notifications
    actor Coworker

    User->>Schedule: View my shift, tap "Find Swap"
    Schedule->>TeamSched: Navigate with shiftId
    TeamSched->>TeamProvider: loadTeamSchedule(weekStart)
    TeamProvider->>TeamRepo: getTeamSchedule(...)
    TeamRepo->>API: GET /team-schedule
    API-->>TeamRepo: Team shifts (with eligibility)
    TeamRepo-->>TeamProvider: List<TeamShift>
    TeamProvider-->>TeamSched: Display team shifts

    User->>TeamSched: Select coworker's shift
    TeamSched->>TeamSched: Show SwapPreview modal
    User->>TeamSched: Add message, tap "Request Swap"

    TeamSched->>SwapProvider: initiateSwapRequest(myShift, theirShift, message)
    SwapProvider->>SwapRepo: initiateSwapRequest(...)
    SwapRepo->>API: POST /requests/swap
    API-->>SwapRepo: 201 Created + SwapRequest
    SwapRepo-->>SwapProvider: ShiftSwapRequest
    SwapProvider-->>TeamSched: SwapSubmissionSuccess
    TeamSched-->>User: Show success, navigate to requests

    API->>FCM: Notify coworker
    FCM-->>Coworker: "Swap Request from [User]" push
```

### Tertiary Flow: Respond to Swap Request (as Coworker)

1. Coworker receives push notification
2. Coworker taps notification → deep links to request detail
3. Coworker sees SwapPreviewCard with both shifts
4. Coworker taps "Accept" or "Decline"
5. If decline: Optional reason input
6. Provider calls repository.respondToSwapRequest()
7. On accept: Request moves to pending_manager, manager notified
8. On decline: Request closed, requestor notified

```mermaid
sequenceDiagram
    actor Coworker
    participant Push as Push Notification
    participant Detail as RequestDetailScreen
    participant Provider as ShiftRequestsProvider
    participant Repo as ShiftRequestsRepository
    participant API as Backend API
    participant FCM as Push Notifications
    actor Requestor
    actor Manager

    Push-->>Coworker: "Swap Request from [User]"
    Coworker->>Push: Tap notification
    Push->>Detail: Deep link to /requests/:requestId
    Detail->>Provider: Load request details
    Provider-->>Detail: ShiftSwapRequest (pending_coworker)

    alt Coworker Accepts
        Coworker->>Detail: Tap "Accept"
        Detail->>Provider: respondToSwapRequest(requestId, accepted: true)
        Provider->>Repo: respondToSwapRequest(...)
        Repo->>API: PUT /requests/swap/:id/respond
        API-->>Repo: Updated SwapRequest (pending_manager)
        Repo-->>Provider: ShiftSwapRequest
        Provider-->>Detail: Update UI to "Awaiting Manager"
        API->>FCM: Notify requestor + manager
        FCM-->>Requestor: "Swap Accepted - Pending Manager"
        FCM-->>Manager: "Swap Request Needs Approval"
    else Coworker Declines
        Coworker->>Detail: Tap "Decline"
        Detail->>Detail: Show reason input (optional)
        Coworker->>Detail: Enter reason, confirm
        Detail->>Provider: respondToSwapRequest(requestId, accepted: false, reason)
        Provider->>Repo: respondToSwapRequest(...)
        Repo->>API: PUT /requests/swap/:id/respond
        API-->>Repo: Updated SwapRequest (declined)
        Repo-->>Provider: ShiftSwapRequest
        Provider-->>Detail: Update UI to "Declined"
        API->>FCM: Notify requestor
        FCM-->>Requestor: "Swap Declined by [Coworker]"
    end
```

### Error Handling

| Error Type | Error Code | User Message | Recovery Action |
|------------|------------|--------------|-----------------|
| Past date selected | `PAST_DATE` | "Cannot request time off for past dates" | Clear date, re-select |
| Too soon (<24h) | `TOO_SOON` | "Requests must be submitted at least 24 hours in advance" | Select later date |
| Duplicate request | `DUPLICATE` | "You already have a pending request for these dates" | Navigate to existing request |
| Shift too soon (<4h) | `SHIFT_TOO_SOON` | "Cannot swap shifts starting in less than 4 hours" | None (blocked) |
| Already pending swap | `ALREADY_PENDING` | "You already have a pending swap for this shift" | Navigate to existing swap |
| Not qualified | `NOT_QUALIFIED` | "You're not qualified for this position" | Select different shift |
| Network error | `NETWORK_ERROR` | "Unable to connect. Check your connection and try again." | Retry button |
| Session expired | `401` | "Your session has expired. Please sign in again." | Redirect to login |
| Server error | `5xx` | "Something went wrong. Please try again later." | Retry button |

### Notification Deep Link Handling

```dart
// Additions to lib/core/services/notification_navigation_service.dart
void _handleNotificationNavigation(NotificationPayload payload) {
  switch (payload.type) {
    case NotificationConstants.typeTimeOffSubmitted:
    case NotificationConstants.typeTimeOffApproved:
    case NotificationConstants.typeTimeOffDenied:
      _router.go('/requests/${payload.data['request_id']}');
      break;

    case NotificationConstants.typeSwapRequestReceived:
    case NotificationConstants.typeSwapAccepted:
    case NotificationConstants.typeSwapDeclined:
    case NotificationConstants.typeSwapApproved:
    case NotificationConstants.typeSwapDenied:
    case NotificationConstants.typeSwapExpired:
      _router.go('/requests/${payload.data['request_id']}');
      break;
  }
}
```

## Deployment View

**No change to existing deployment** - This feature adds new Flutter code that compiles into existing iOS/Android apps. No infrastructure changes required.

- **Environment**: Mobile app (iOS/Android)
- **Configuration**: No new environment variables
- **Dependencies**: No new external services (uses existing FCM, backend API)
- **Performance**:
  - Target: <500ms for request list load
  - Target: <1s for request submission
  - Caching: In-memory cache of requests, refresh on pull-to-refresh or notification

---

## PRD → Design Traceability

### Business Rule Ownership (Client vs Backend)

| PRD Rule | Owner | Client Behavior | Backend Behavior |
|----------|-------|-----------------|------------------|
| Cannot request time off for past dates | **Client + Backend** | Client pre-validates, disables past dates in picker | Backend returns `PAST_DATE` error |
| 24-hour advance notice (configurable) | **Backend** | Client shows warning if <24h (soft) | Backend enforces store config, returns `TOO_SOON` |
| Maximum 14-day request length | **Backend** | Client could warn but doesn't block | Backend returns `MAX_RANGE_EXCEEDED` |
| Contiguous date range only | **Client** | Date picker enforces contiguous selection | Backend validates gap-free range |
| No duplicate/overlapping pending requests | **Backend** | N/A (server source of truth) | Backend returns `DUPLICATE` or `OVERLAP` |
| Cannot time-off when pending swap exists | **Backend** | N/A (complex cross-query) | Backend returns `PENDING_SWAP_CONFLICT` |
| Warn if already scheduled on date | **Backend** | Client displays warning from API response | Backend includes `hasScheduledShift: true` in validation |
| Swap: 4-hour cutoff before shift start | **Backend** | Client hides "Find Swap" if <4h (using local time) | Backend returns `SHIFT_TOO_SOON` |
| Swap: Position qualification | **Backend** | Client shows `isEligibleForSwap` from API | Backend calculates eligibility |
| Swap: Overtime warning | **Backend** | Client displays warning from API response | Backend includes `wouldCauseOvertime: true` |
| Swap: 24-hour coworker response window | **Backend** | Client shows expiration countdown | Backend auto-expires via cron/scheduler |
| Swap: One pending request per shift | **Backend** | N/A | Backend returns `ALREADY_PENDING` |
| Store timezone for all dates | **Backend** | Client sends dates without time; displays in store TZ | Backend stores/returns all dates in store timezone |

### Acceptance Criteria Coverage

| PRD Feature | Acceptance Criterion | SDD Component | Test Type |
|-------------|---------------------|---------------|-----------|
| F1: Day Off Submit | Can select single date or date range | TimeOffRequestScreen date picker | Widget test |
| F1: Day Off Submit | Can add optional reason | TimeOffRequestScreen form | Widget test |
| F1: Day Off Submit | Request submitted to manager | ShiftRequestsRepository.submitTimeOffRequest | Unit test |
| F1: Day Off Submit | Receives confirmation (in-app + push) | Provider + FCM | Integration test |
| F1: Day Off Submit | Request appears in pending list | ShiftRequestsProvider state | Provider test |
| F2: Status Tracking | View all requests (pending/approved/denied) | RequestsScreen tabs | Widget test |
| F2: Status Tracking | Shows dates, reason, status, date | RequestCard widget | Widget test |
| F2: Status Tracking | Denied shows manager reason | RequestCard / DetailScreen | Widget test |
| F2: Status Tracking | Push on status change | Notification handler | Integration test |
| F3: Shift Swap | Initiate from scheduled shift | ScheduleScreen → TeamScheduleScreen | Integration test |
| F3: Shift Swap | View team schedule | TeamScheduleScreen | Widget test |
| F3: Shift Swap | Select coworker shift | TeamShiftCard selection | Widget test |
| F3: Shift Swap | Coworker receives push | FCM backend | E2E test |
| F3: Shift Swap | Manager notified after accept | FCM backend | E2E test |
| F4: Team Schedule | Week view navigation | TeamScheduleScreen | Widget test |
| F4: Team Schedule | Filter by position | Position filter dropdown | Widget test |
| F4: Team Schedule | Shows eligibility | TeamShiftCard.isEligible | Widget test |
| F4: Team Schedule | Privacy respected | Data model fields | Unit test |
| F5: Respond to Swap | Receives push notification | FCM handler | Integration test |
| F5: Respond to Swap | View proposal details | RequestDetailScreen | Widget test |
| F5: Respond to Swap | Accept/Decline actions | Provider.respondToSwapRequest | Provider test |
| F6: Push Notifications | All notification types | NotificationConstants | Unit test |
| F6: Push Notifications | Deep links to screens | NotificationNavigationService | Integration test |
| F6: Push Notifications | Preferences in Settings | NotificationSettingsScreen (existing) | N/A - uses existing |
| F7: Request History | View past time-off | RequestsScreen history tab | Widget test |
| F7: Request History | View past swaps | RequestsScreen history tab | Widget test |
| F7: Request History | Filter by date/status | Filter UI + API params | Widget + Integration test |
| F8: Cancel Request | Cancel pending time-off | Provider.cancelTimeOffRequest | Provider test |
| F8: Cancel Request | Withdraw pending swap | Provider.cancelSwapRequest | Provider test |
| F11: Approval Visibility | Shows "Awaiting [Name]" | RequestCard.awaitingAction | Widget test |
| F12: Offline Fallback | Pull-to-refresh | RequestsScreen RefreshIndicator | Widget test |
| F12: Offline Fallback | Badge count | Bottom nav badge | Widget test |

---

## Notification Preferences Integration

**Note:** Notification preferences are managed via the **existing NotificationSettingsScreen** (`lib/presentation/screens/settings/notification_settings_screen.dart`).

### Required Additions to Existing Preferences

The existing `NotificationPreferencesModel` in `lib/data/models/notification_preferences_model.dart` already supports category toggles. This feature adds:

```dart
// Extend NotificationPreferencesModel (if not already present)
@Default(true) bool requestsEnabled,     // Master toggle for all request notifications
@Default(true) bool swapRequestsEnabled, // Swap-specific notifications
```

### Notification Preference Mapping

| Notification Type | Preference Check | Fallback |
|-------------------|------------------|----------|
| time_off_submitted | `requestsEnabled` | In-app only |
| time_off_approved | `requestsEnabled` | In-app only |
| time_off_denied | `requestsEnabled` | In-app only |
| swap_request_received | `swapRequestsEnabled && requestsEnabled` | In-app only |
| swap_accepted | `swapRequestsEnabled && requestsEnabled` | In-app only |
| swap_declined | `swapRequestsEnabled && requestsEnabled` | In-app only |
| swap_approved | `swapRequestsEnabled && requestsEnabled` | In-app only |
| swap_denied | `swapRequestsEnabled && requestsEnabled` | In-app only |
| swap_expired | `swapRequestsEnabled && requestsEnabled` | In-app only |

---

## Data Refresh Strategy (Push Fallback)

### Pull-to-Refresh (Required)

All request list screens implement `RefreshIndicator`:

```dart
RefreshIndicator(
  onRefresh: () => ref.read(shiftRequestsProvider.notifier).refresh(),
  child: ListView(...),
)
```

### Auto-Refresh on Screen Focus

When push notifications are disabled or unreliable:

```dart
class RequestsScreen extends ConsumerStatefulWidget {
  @override
  ConsumerState<RequestsScreen> createState() => _RequestsScreenState();
}

class _RequestsScreenState extends ConsumerState<RequestsScreen>
    with WidgetsBindingObserver {

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) {
      // Refresh when app returns to foreground
      ref.read(shiftRequestsProvider.notifier).refreshIfStale();
    }
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}
```

### Staleness Check

```dart
void refreshIfStale() {
  final currentState = state;
  if (currentState is ShiftRequestsLoaded) {
    final age = DateTime.now().difference(currentState.lastUpdated);
    if (age > const Duration(minutes: 5)) {
      refresh();
    }
  }
}
```

### Notification Handler Triggers Refresh

```dart
// In PushNotificationService or NotificationProvider
void _handleRequestNotification(NotificationPayload payload) {
  // Always refresh request list when relevant notification received
  if (_isRequestNotificationType(payload.type)) {
    ref.read(shiftRequestsProvider.notifier).refresh();
  }
}
```

---

## API Contract Specifications

### Time-Off Request Endpoints

#### POST `/api/mobile/scheduling/:typeNum/requests/time-off`

**Request:**
```json
{
  "startDate": "2025-01-15",           // ISO 8601 date (store timezone)
  "endDate": "2025-01-17",             // ISO 8601 date (store timezone)
  "reason": "Doctor appointment",       // Optional, max 500 chars
  "idempotencyKey": "uuid-v4-string"   // Required for retry safety
}
```

**Success Response (201 Created):**
```json
{
  "requestId": 12345,
  "startDate": "2025-01-15",
  "endDate": "2025-01-17",
  "reason": "Doctor appointment",
  "status": "pending",
  "submittedAt": "2025-01-10T14:30:00Z",
  "hasScheduledShift": true,           // Warning: user has shift on these dates
  "scheduledShiftDates": ["2025-01-15"]
}
```

**Error Responses:**

| Status | Code | Message | When |
|--------|------|---------|------|
| 400 | `PAST_DATE` | Cannot request time off for past dates | Start date < today |
| 400 | `TOO_SOON` | Requests must be submitted at least 24 hours in advance | < store min advance |
| 400 | `MAX_RANGE_EXCEEDED` | Maximum request length is 14 days | Range > store max |
| 400 | `DUPLICATE` | You already have a pending request for these dates | Exact overlap |
| 400 | `OVERLAP` | These dates overlap with an existing request | Partial overlap |
| 400 | `PENDING_SWAP_CONFLICT` | Cannot request time off for dates with pending swap | Swap exists |
| 409 | `IDEMPOTENCY_CONFLICT` | Request already processed | Replay with different data |

#### GET `/api/mobile/scheduling/:typeNum/requests/time-off`

**Query Parameters:**
```
?status=pending,approved,denied,cancelled  // Comma-separated filter
&startDate=2025-01-01                      // Filter requests starting on or after
&endDate=2025-03-31                        // Filter requests ending on or before
&limit=50                                  // Pagination limit (default 50)
&cursor=eyJpZCI6MTIzfQ                     // Pagination cursor (opaque)
```

**Response:**
```json
{
  "requests": [...],
  "pagination": {
    "hasMore": true,
    "nextCursor": "eyJpZCI6MTAwfQ"
  }
}
```

### Shift Swap Request Endpoints

#### POST `/api/mobile/scheduling/:typeNum/requests/swap`

**Request:**
```json
{
  "myShiftId": 456,
  "targetShiftId": 789,
  "message": "Would you mind swapping? I have a conflict.",  // Optional
  "idempotencyKey": "uuid-v4-string"
}
```

**Success Response (201 Created):**
```json
{
  "requestId": 67890,
  "requestorShift": { /* ShiftSummary */ },
  "targetShift": { /* ShiftSummary */ },
  "requestor": { "odooUserId": "123", "firstName": "John", "photoUrl": null },
  "target": { "odooUserId": "456", "firstName": "Jane", "photoUrl": "https://..." },
  "message": "Would you mind swapping?",
  "status": "pending_coworker",
  "createdAt": "2025-01-10T14:30:00Z",
  "expiresAt": "2025-01-11T14:30:00Z",
  "wouldCauseOvertime": false,
  "overtimeHoursRequestor": 0,
  "overtimeHoursTarget": 0
}
```

**Error Responses:**

| Status | Code | Message | When |
|--------|------|---------|------|
| 400 | `SHIFT_TOO_SOON` | Cannot swap shifts starting in less than 4 hours | < 4h to start |
| 400 | `ALREADY_PENDING` | You already have a pending swap for this shift | Existing pending |
| 400 | `NOT_QUALIFIED` | You're not qualified for this position | Position mismatch |
| 400 | `SAME_SHIFT` | Cannot swap a shift with itself | Edge case |
| 400 | `INVALID_SHIFT` | Shift not found or not swappable | Deleted/past |
| 404 | `TARGET_NOT_FOUND` | Target employee not found | Terminated |

#### PUT `/api/mobile/scheduling/:typeNum/requests/swap/:requestId/respond`

**Request:**
```json
{
  "accepted": true,
  "declineReason": null  // Required if accepted=false, max 200 chars
}
```

**Response:** Updated `ShiftSwapRequestModel`

### Notification Payload Schema

```json
{
  "type": "swap_request_received",       // Notification type constant
  "title": "Swap Request",               // Display title
  "body": "John wants to swap shifts",   // Display body
  "data": {
    "requestType": "swap",               // "time_off" or "swap"
    "requestId": "67890",                // String for FCM compatibility
    "typeNum": "12345",                  // Store context
    "sentAt": "2025-01-10T14:30:00Z"     // For ordering
  }
}
```

### Idempotency Implementation

- **Header:** `X-Idempotency-Key: <uuid-v4>`
- **Window:** 24 hours
- **Behavior:**
  - First request with key: Process normally, store result
  - Repeat with same key + same data: Return cached result (200 OK)
  - Repeat with same key + different data: Return 409 Conflict
- **Client Storage:** Store pending idempotency keys in `SharedPreferences` with expiration

---

## Team Schedule Privacy Specification

### Allowed Fields (Returned by API)

| Field | Returned | Displayed | Notes |
|-------|----------|-----------|-------|
| `employeeFirstName` | ✅ | ✅ | First name only |
| `employeeLastName` | ❌ | ❌ | Never returned |
| `employeePhotoUrl` | ✅ | ✅ | If set, optional |
| `shiftId` | ✅ | Internal | For swap selection |
| `date` | ✅ | ✅ | |
| `startTime` / `endTime` | ✅ | ✅ | |
| `durationHours` | ✅ | ✅ | |
| `positionName` | ✅ | ✅ | For filtering |
| `positionId` | ✅ | Internal | For filtering |
| `employeePhone` | ❌ | ❌ | Never returned |
| `employeeEmail` | ❌ | ❌ | Never returned |
| `totalWeeklyHours` | ❌ | ❌ | Privacy concern |

### Deep Link Authorization

When handling notification deep links:

```dart
void _handleDeepLink(String typeNum, String requestId) async {
  final selectedStore = ref.read(selectedStoreProvider);

  if (selectedStore?.typeNum != typeNum) {
    // Option 1: Switch store if user has access
    final stores = ref.read(userStoresProvider);
    final targetStore = stores.firstWhereOrNull((s) => s.typeNum == typeNum);

    if (targetStore != null) {
      await ref.read(storeProvider.notifier).selectStore(targetStore);
      _navigateToRequest(requestId);
    } else {
      // Option 2: Show error if no access
      _showError('This request is for a different store');
    }
  } else {
    _navigateToRequest(requestId);
  }
}
```

---

## Swap Request Filter

Add to repository interface:

```dart
class SwapRequestFilter extends Equatable {
  final ShiftSwapStatus? status;
  final DateTime? createdAfter;
  final DateTime? createdBefore;
  final bool? initiatedByMe;  // true = I initiated, false = sent to me

  const SwapRequestFilter({
    this.status,
    this.createdAfter,
    this.createdBefore,
    this.initiatedByMe,
  });

  @override
  List<Object?> get props => [status, createdAfter, createdBefore, initiatedByMe];
}

// Add to ShiftRequestsRepository
Future<List<ShiftSwapRequest>> getSwapRequests({
  required String typeNum,
  SwapRequestFilter? filter,  // NEW
});
```

---

## Timezone Handling

### Principles

1. **All dates are store-local**: The backend stores and returns dates in the store's timezone
2. **Client displays as-is**: No conversion needed for display
3. **Date-only fields**: `startDate`, `endDate`, `date` are date strings (`YYYY-MM-DD`), not datetimes
4. **Time fields**: `startTime`, `endTime` are time strings (`HH:mm`), assumed store-local

### DST Edge Cases

- Backend handles DST transitions when calculating durations
- Client displays times as returned (no adjustment needed)
- Shift start/end times are stored in wall-clock time, not UTC offsets

### Parsing Strategy

```dart
// In entity constructors / mappers
DateTime _parseDate(String dateStr) {
  // Parse as local date (store timezone assumed)
  return DateTime.parse(dateStr);
}

String _formatDate(DateTime date) {
  // Format as date only
  return DateFormat('yyyy-MM-dd').format(date);
}
```

---

## Offline Queue Component

### Component: OfflineQueueService

**Location:** `lib/core/services/offline_queue_service.dart`

**Responsibilities:**
- Queue failed requests when offline
- Persist queue to `SharedPreferences`
- Retry on connectivity restoration
- Deduplicate using idempotency keys
- Update UI state during queue processing

**Interface:**
```dart
abstract class OfflineQueueService {
  Future<void> enqueue(QueuedRequest request);
  Future<void> processQueue();
  Stream<QueueStatus> get statusStream;
  Future<List<QueuedRequest>> getPendingRequests();
}

class QueuedRequest {
  final String id;
  final String idempotencyKey;
  final String endpoint;
  final Map<String, dynamic> payload;
  final DateTime enqueuedAt;
  final int retryCount;
  final QueuedRequestType type;
}

enum QueuedRequestType {
  timeOffSubmit,
  timeOffCancel,
  swapInitiate,
  swapRespond,
  swapCancel,
}

enum QueueStatus {
  idle,
  processing,
  failed,
}
```

**UI States:**
- **Queued:** "Request saved. Will submit when online." (info banner)
- **Retrying:** "Submitting saved requests..." (loading indicator)
- **Failed:** "Some requests couldn't be submitted. Tap to retry." (error banner with action)

**Conflict Resolution:**
- On reconnect, check if server already has the request (via idempotency key)
- If server returns 200 with cached result, treat as success
- If server returns 409 Conflict, show user error and remove from queue

## Cross-Cutting Concepts

### Pattern Documentation

```yaml
# Existing patterns used
- pattern: Riverpod Notifier pattern
  relevance: CRITICAL
  why: "All state management follows this pattern"

- pattern: Sealed class state machines
  relevance: CRITICAL
  why: "Request states and submission states use this pattern"

- pattern: Repository pattern with Dio
  relevance: HIGH
  why: "All API calls go through repository implementations"

- pattern: Freezed for data models
  relevance: HIGH
  why: "All API response models use Freezed"

- pattern: Equatable for domain entities
  relevance: HIGH
  why: "All domain entities extend Equatable"
```

### System-Wide Patterns Applied

**Security:**
- JWT authentication via existing AuthInterceptor
- Store-scoped requests (can only request within assigned store)
- No sensitive data exposed in push notifications (just IDs)

**Error Handling:**
- Custom exception hierarchy (AuthException, NetworkException, ValidationException, ApiException)
- User-friendly error messages with recovery actions
- Errors logged for debugging

**Performance:**
- Optimistic updates for submissions (show immediately, revert on error)
- In-memory caching of request lists
- Pagination for large request histories (if needed)

**Logging/Auditing:**
- Analytics events per PRD tracking requirements
- Error logging for debugging

### Implementation Patterns

#### State Management Pattern

```dart
// Provider structure
final shiftRequestsProvider = NotifierProvider<ShiftRequestsNotifier, ShiftRequestsState>(
  ShiftRequestsNotifier.new,
);

// Derived providers for filtered views
final pendingTimeOffRequestsProvider = Provider<List<TimeOffRequest>>((ref) {
  final state = ref.watch(shiftRequestsProvider);
  if (state is ShiftRequestsLoaded) {
    return state.timeOffRequests.where((r) => r.isPending).toList();
  }
  return [];
});

final pendingSwapRequestsProvider = Provider<List<ShiftSwapRequest>>((ref) {
  final state = ref.watch(shiftRequestsProvider);
  if (state is ShiftRequestsLoaded) {
    return state.swapRequests.where((r) => r.isPending).toList();
  }
  return [];
});

final actionRequiredSwapsProvider = Provider<List<ShiftSwapRequest>>((ref) {
  final state = ref.watch(shiftRequestsProvider);
  final currentUser = ref.watch(currentUserProvider);
  if (state is ShiftRequestsLoaded && currentUser != null) {
    return state.swapRequests.where((r) =>
      r.isPendingCoworker && r.target.odooUserId == currentUser.odooUserId
    ).toList();
  }
  return [];
});
```

#### Error Handling Pattern

```dart
class _RequestErrorMessages {
  static const String networkError =
      'Unable to connect. Please check your internet connection and try again.';
  static const String sessionExpired =
      'Your session has expired. Please sign in again.';
  static const String serverError =
      'Something went wrong. Please try again later.';
  static const String pastDateError =
      'Cannot request time off for past dates.';
  static const String tooSoonError =
      'Requests must be submitted at least 24 hours in advance.';
  static const String duplicateError =
      'You already have a pending request for these dates.';
}

// In Notifier
Future<void> submitTimeOffRequest(...) async {
  state = TimeOffSubmissionSubmitting();
  try {
    final request = await _repo.submitTimeOffRequest(...);
    state = TimeOffSubmissionSuccess(request);
    _addToLoadedList(request);
  } on ValidationException catch (e) {
    final type = _mapValidationError(e.code);
    state = TimeOffSubmissionError(message: e.message, type: type);
  } on AuthException {
    state = TimeOffSubmissionError(
      message: _RequestErrorMessages.sessionExpired,
      type: TimeOffSubmissionErrorType.network,
    );
    await ref.read(authProvider.notifier).handleAccessRevoked();
  } on NetworkException {
    state = TimeOffSubmissionError(
      message: _RequestErrorMessages.networkError,
      type: TimeOffSubmissionErrorType.network,
    );
  } catch (e) {
    state = TimeOffSubmissionError(
      message: _RequestErrorMessages.serverError,
      type: TimeOffSubmissionErrorType.unknown,
    );
  }
}
```

#### Test Pattern

```dart
// Provider tests
test('submitTimeOffRequest success', () async {
  final container = ProviderContainer(
    overrides: [
      shiftRequestsRepositoryProvider.overrideWithValue(mockRepo),
    ],
  );

  when(mockRepo.submitTimeOffRequest(
    typeNum: '12345',
    startDate: DateTime(2025, 1, 15),
    endDate: DateTime(2025, 1, 15),
    reason: 'Doctor appointment',
  )).thenAnswer((_) async => mockTimeOffRequest);

  await container.read(shiftRequestsProvider.notifier).submitTimeOffRequest(
    startDate: DateTime(2025, 1, 15),
    endDate: DateTime(2025, 1, 15),
    reason: 'Doctor appointment',
  );

  final state = container.read(timeOffSubmissionProvider);
  expect(state, isA<TimeOffSubmissionSuccess>());
});
```

## Architecture Decisions

- [x] **ADR-1 Single Provider vs Multiple Providers**: Use single `ShiftRequestsProvider` for all request operations
  - Rationale: Simplifies state management, allows atomic operations on related data
  - Trade-offs: Larger state object, but derived providers keep UI simple
  - Alternatives considered: Separate providers per request type (more complex coordination)
  - User confirmed: ✅ 2025-12-31

- [x] **ADR-2 Team Schedule as Separate Provider**: Use dedicated `TeamScheduleProvider` for browsing coworker shifts
  - Rationale: Team schedule is fetched independently, different caching needs
  - Trade-offs: Two providers to coordinate, but clearer separation
  - Alternatives considered: Include in ShiftRequestsProvider (bloated state)
  - User confirmed: ✅ 2025-12-31

- [x] **ADR-3 Optimistic Updates for Submissions**: Show submitted requests immediately, revert on error
  - Rationale: Better UX, immediate feedback, common mobile pattern
  - Trade-offs: Must handle rollback on error, potential temporary inconsistency
  - Alternatives considered: Wait for server response (slower UX)
  - User confirmed: ✅ 2025-12-31

- [x] **ADR-4 Backend-Calculated Swap Eligibility**: Backend returns `isEligibleForSwap` per team shift
  - Rationale: Business rules (position qualification, overtime) are complex, backend has all data
  - Trade-offs: Extra field in API response, but simplifies client logic
  - Alternatives considered: Client-side eligibility calculation (complex, potential inconsistency)
  - User confirmed: ✅ 2025-12-31

- [x] **ADR-5 Sealed Classes for All States**: Use sealed class hierarchies for request states, submission states, and team schedule states
  - Rationale: Type-safe, exhaustive pattern matching, clear state transitions
  - Trade-offs: More boilerplate than simple enums, but much safer
  - Alternatives considered: Simple enum + nullable fields (error-prone)
  - User confirmed: ✅ 2025-12-31 (follows existing codebase pattern - implicit approval)

## Quality Requirements

| Requirement | Target | Measurement |
|-------------|--------|-------------|
| Request list load time | <500ms | 95th percentile API response + render |
| Request submission time | <1s | Time from tap to confirmation |
| Offline submission queue | 100% reliable | Queued requests delivered when online |
| Push notification delivery | >95% | FCM delivery reports |
| Crash-free sessions | >99.5% | Firebase Crashlytics |
| Form validation errors | <5% false positives | User complaints, support tickets |

### Accessibility
- All interactive elements have semantic labels
- Color is not the only status indicator (icons + text)
- Form fields have proper labels and error messages
- Touch targets minimum 48x48dp

### Security
- No sensitive data logged or stored insecurely
- Request IDs only in notifications (not details)
- Store-scoped access enforced by backend

## Risks and Technical Debt

### Known Technical Issues
- None specific to this feature (greenfield implementation)

### Technical Debt
- May need pagination for request history if users accumulate many requests
- Team schedule filtering could be enhanced with date range selection

### Implementation Gotchas
- **Timezone handling**: All dates stored/transmitted in store timezone, not device timezone
- **Swap expiration**: Backend handles 24h expiration, client refreshes on notification
- **Concurrent modifications**: Backend uses optimistic locking for swap responses
- **Notification ordering**: Notifications may arrive out of order; use timestamps to sort

## Test Specifications

### Critical Test Scenarios

**Scenario 1: Submit Time-Off Request (Happy Path)**
```gherkin
Given: User is authenticated with a selected store
And: No pending request exists for the selected dates
When: User submits a time-off request for January 15, 2025
Then: Request appears in pending list immediately
And: User receives confirmation push notification
And: Request status is "pending"
```

**Scenario 2: Duplicate Time-Off Request Prevention**
```gherkin
Given: User has a pending request for January 15, 2025
When: User tries to submit another request for January 15, 2025
Then: Error message "You already have a pending request for these dates" is shown
And: User is not able to submit the duplicate request
```

**Scenario 3: Initiate Swap Request (Happy Path)**
```gherkin
Given: User has a scheduled shift on January 15, 2025
And: Coworker has a compatible shift on January 16, 2025
When: User selects coworker's shift and requests swap
Then: Swap request is created with status "pending_coworker"
And: Coworker receives push notification
And: User sees swap in their pending requests
```

**Scenario 4: Coworker Accepts Swap**
```gherkin
Given: User has a pending swap request sent to Coworker
And: Swap status is "pending_coworker"
When: Coworker accepts the swap request
Then: Swap status changes to "pending_manager"
And: Manager receives approval notification
And: User receives notification that coworker accepted
```

**Scenario 5: Network Error Recovery**
```gherkin
Given: User is filling out time-off request form
And: Network connection is lost
When: User attempts to submit the request
Then: Error message about network is shown
And: Retry button is available
When: Network is restored and user taps retry
Then: Request is submitted successfully
```

**Scenario 6: Cancel Pending Request**
```gherkin
Given: User has a pending time-off request
When: User cancels the request
Then: Request status changes to "cancelled"
And: Request moves to history
And: Manager is notified of cancellation (if already viewed)
```

### Test Coverage Requirements

- **Business Logic**: All state transitions, eligibility rules, date validation
- **User Interface**: Form validation, error states, loading states, empty states
- **Integration Points**: API calls, notification handling, deep linking
- **Edge Cases**: Timezone boundaries, concurrent modifications, expiration
- **Performance**: List rendering with 100+ requests, rapid submissions
- **Security**: Store-scoping, auth token handling

---

## Glossary

### Domain Terms

| Term | Definition | Context |
|------|------------|---------|
| Time-Off Request | A request for one or more consecutive days off work | Core feature - allows employees to request vacation, personal days, etc. |
| Shift Swap | An exchange of scheduled shifts between two employees | Requires coworker acceptance then manager approval |
| Team Schedule | View of all employees' scheduled shifts at a store | Used to find swap partners |
| Pending | Request submitted but not yet reviewed/responded | Requests in this state can be cancelled |
| Coworker | Another employee at the same store | Swap target who must accept before manager reviews |

### Technical Terms

| Term | Definition | Context |
|------|------------|---------|
| typeNum | Store identifier number used in API endpoints | All requests are scoped to a specific store |
| Sealed Class | Dart class that restricts which classes can extend it | Used for exhaustive state machines |
| Optimistic Update | UI update before server confirmation | Shows request immediately, reverts on error |
| Deep Link | URL that opens specific app screen | Push notifications use deep links to navigate to request details |
| Notifier | Riverpod 3.x state management class | Replaces deprecated StateNotifier |

### API Terms

| Term | Definition | Context |
|------|------------|---------|
| 201 Created | HTTP status for successful resource creation | Returned when request/swap is created |
| Idempotency Key | Unique key to prevent duplicate submissions | Prevents double-submission on network retry |
| Bearer Token | JWT authentication token in Authorization header | Used for all authenticated API calls |
