# Solution Design Document

## Validation Checklist

- [x] All required sections are complete
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Architecture pattern is clearly stated with rationale
- [x] Every component has directory mapping
- [x] Every interface has specification
- [x] Data models follow Freezed 3.x patterns
- [x] Domain entities follow Equatable patterns
- [x] Error handling covers all error types
- [x] Quality requirements are specific and measurable
- [x] A developer could implement from this design

---

## 1. Technical Summary

### Overview
The Close Reports frontend feature provides store managers mobile access to daily close reports with historical navigation and comparison capabilities. The implementation follows the established Clean Architecture with Riverpod pattern used throughout the BuyerKiosk Live app.

### Key Technical Decisions

| Decision | Choice | Rationale |
|----------|--------|-----------|
| API Client | Reuse existing `SchedulingApiClient` | Close reports API uses same JWT auth as scheduling; avoid duplication |
| Data Models | Freezed 3.x | Consistent with existing models; immutable, code generation |
| Domain Entities | Equatable | Consistent with existing entities; value equality |
| State Management | Riverpod 3.x AsyncNotifier | Consistent with existing patterns; family providers for store-scoped data |
| Navigation | GoRouter | Existing router; add close-reports routes under store branch |
| Monetary Values | Convert cents to dollars at mapper layer | API returns cents; UI displays dollars with 2 decimal places |

### Architecture Pattern
**Clean Architecture with Riverpod** following the existing app structure:
- **Data Layer**: Freezed models, remote datasource, repository implementation
- **Domain Layer**: Equatable entities, repository interface
- **Presentation Layer**: Riverpod providers (AsyncNotifier), screens, widgets

---

## 2. Data Models (Freezed 3.x)

All models use the Freezed 3.x pattern with `abstract class` and include JSON serialization.

### 2.1 CloseReportSummaryModel

File: `lib/data/models/close_reports/close_report_summary_model.dart`

```dart
@freezed
abstract class CloseReportSummaryModel with _$CloseReportSummaryModel {
  const factory CloseReportSummaryModel({
    required int id,
    required String reportDate,
    required String postedAt,
    @JsonKey(fromJson: _parseInt) required int netSalesRetail,
    @JsonKey(fromJson: _parseDouble) required double salesVsGoalPercent,
    @JsonKey(fromJson: _parseInt) required int buysCount,
    @JsonKey(fromJson: _parseInt) required int buysTotal,
    required bool hasDiscrepancy,
    @JsonKey(fromJson: _parseDouble) required double laborPercent,
  }) = _CloseReportSummaryModel;

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

### 2.2 CloseReportDetailModel

File: `lib/data/models/close_reports/close_report_detail_model.dart`

```dart
@freezed
abstract class CloseReportDetailModel with _$CloseReportDetailModel {
  const factory CloseReportDetailModel({
    required CloseReportMetadataModel metadata,
    required SalesSummaryModel salesSummary,
    required BuysSummaryModel buys,
    required ReturnsSummaryModel returns,
    required LaborSummaryModel labor,
  }) = _CloseReportDetailModel;

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

@freezed
abstract class CloseReportMetadataModel with _$CloseReportMetadataModel {
  const factory CloseReportMetadataModel({
    required int id,
    required String reportDate,
    required String postedAt,
    required String typeNum,
  }) = _CloseReportMetadataModel;

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

@freezed
abstract class SalesSummaryModel with _$SalesSummaryModel {
  const factory SalesSummaryModel({
    @JsonKey(fromJson: _parseInt) required int grossSalesRetail,
    @JsonKey(fromJson: _parseInt) required int grossSalesCost,
    @JsonKey(fromJson: _parseInt) required int grossSalesGM,
    @JsonKey(fromJson: _parseInt) required int netSalesRetail,
    @JsonKey(fromJson: _parseInt) required int netSalesCost,
    @JsonKey(fromJson: _parseInt) required int netSalesGM,
    @JsonKey(fromJson: _parseInt) required int salesCount,
    @JsonKey(fromJson: _parseInt) required int averageRetail,
    @JsonKey(fromJson: _parseInt) required int salesGoal,
    @JsonKey(fromJson: _parseDouble) required double salesVsGoalPercent,
  }) = _SalesSummaryModel;

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

@freezed
abstract class BuysSummaryModel with _$BuysSummaryModel {
  const factory BuysSummaryModel({
    @JsonKey(fromJson: _parseInt) required int buysCost,
    @JsonKey(fromJson: _parseInt) required int buysRetail,
    @JsonKey(fromJson: _parseInt) required int buysGM,
    @JsonKey(fromJson: _parseInt) required int buysCount,
    @JsonKey(fromJson: _parseInt) required int buysGoal,
    @JsonKey(fromJson: _parseInt) required int buysOutstanding,
    @JsonKey(fromJson: _parseDouble) required double buysVsGoalPercent,
  }) = _BuysSummaryModel;

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

@freezed
abstract class ReturnsSummaryModel with _$ReturnsSummaryModel {
  const factory ReturnsSummaryModel({
    @JsonKey(fromJson: _parseInt) required int returnsCost,
    @JsonKey(fromJson: _parseInt) required int returnsRetail,
    @JsonKey(fromJson: _parseInt) required int returnsNumber,
    @JsonKey(fromJson: _parseInt) required int returnsGM,
  }) = _ReturnsSummaryModel;

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

@freezed
abstract class LaborSummaryModel with _$LaborSummaryModel {
  const factory LaborSummaryModel({
    @JsonKey(fromJson: _parseDouble) required double laborPercentage,
    @JsonKey(fromJson: _parseDouble) required double laborHours,
    @JsonKey(fromJson: _parseInt) required int laborDollars,
  }) = _LaborSummaryModel;

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

### 2.3 CloseReportComparisonModel

File: `lib/data/models/close_reports/close_report_comparison_model.dart`

```dart
@freezed
abstract class CloseReportComparisonModel with _$CloseReportComparisonModel {
  const factory CloseReportComparisonModel({
    required CloseReportSummaryModel primary,
    CloseReportSummaryModel? comparison,
    ComparisonDeltasModel? deltas,
    @Default([]) List<ComparisonHighlightModel> highlights,
  }) = _CloseReportComparisonModel;

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

@freezed
abstract class ComparisonDeltasModel with _$ComparisonDeltasModel {
  const factory ComparisonDeltasModel({
    @JsonKey(fromJson: _parseInt) required int netSalesRetail,
    @JsonKey(fromJson: _parseDouble) required double netSalesRetailPercent,
    @JsonKey(fromJson: _parseInt) required int buysCount,
    @JsonKey(fromJson: _parseDouble) required double buysCountPercent,
    @JsonKey(fromJson: _parseInt) required int buysTotal,
    @JsonKey(fromJson: _parseDouble) required double buysTotalPercent,
    @JsonKey(fromJson: _parseDouble) required double salesVsGoalPercent,
    @JsonKey(fromJson: _parseDouble) required double laborPercent,
  }) = _ComparisonDeltasModel;

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

@freezed
abstract class ComparisonHighlightModel with _$ComparisonHighlightModel {
  const factory ComparisonHighlightModel({
    required String field,
    required String label,
    required String direction,
    @JsonKey(fromJson: _parseDouble) required double percentChange,
    required String significance,
  }) = _ComparisonHighlightModel;

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

### 2.4 CalendarDatesModel

File: `lib/data/models/close_reports/calendar_dates_model.dart`

```dart
@freezed
abstract class CalendarDatesModel with _$CalendarDatesModel {
  const factory CalendarDatesModel({
    required List<String> dates,
    required int year,
    int? month,
  }) = _CalendarDatesModel;

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

### 2.5 Paginated Response Model

File: `lib/data/models/close_reports/close_report_list_response_model.dart`

```dart
@freezed
abstract class CloseReportListResponseModel with _$CloseReportListResponseModel {
  const factory CloseReportListResponseModel({
    required List<CloseReportSummaryModel> reports,
    required PaginationModel pagination,
  }) = _CloseReportListResponseModel;

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

@freezed
abstract class PaginationModel with _$PaginationModel {
  const factory PaginationModel({
    required int total,
    required int limit,
    required int offset,
    required bool hasMore,
  }) = _PaginationModel;

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

### 2.6 Barrel Export

File: `lib/data/models/close_reports/close_report_models.dart`

```dart
export 'close_report_summary_model.dart';
export 'close_report_detail_model.dart';
export 'close_report_comparison_model.dart';
export 'calendar_dates_model.dart';
export 'close_report_list_response_model.dart';
```

---

## 3. Domain Entities (Equatable)

All entities use the Equatable pattern for value equality comparison.

### 3.1 CloseReportSummary

File: `lib/domain/entities/close_reports/close_report_summary.dart`

```dart
class CloseReportSummary extends Equatable {
  final int id;
  final DateTime reportDate;
  final DateTime postedAt;
  final double netSalesRetail;  // Converted from cents to dollars
  final double salesVsGoalPercent;
  final int buysCount;
  final double buysTotal;  // Converted from cents to dollars
  final bool hasDiscrepancy;
  final double laborPercent;

  const CloseReportSummary({
    required this.id,
    required this.reportDate,
    required this.postedAt,
    required this.netSalesRetail,
    required this.salesVsGoalPercent,
    required this.buysCount,
    required this.buysTotal,
    required this.hasDiscrepancy,
    required this.laborPercent,
  });

  /// Formatted net sales as currency string
  String get netSalesFormatted => '\$${netSalesRetail.toStringAsFixed(2)}';

  /// Formatted buys total as currency string
  String get buysTotalFormatted => '\$${buysTotal.toStringAsFixed(2)}';

  /// Formatted sales vs goal percentage
  String get salesVsGoalFormatted => '${salesVsGoalPercent.toStringAsFixed(1)}%';

  /// Formatted labor percentage
  String get laborPercentFormatted => '${laborPercent.toStringAsFixed(1)}%';

  @override
  List<Object?> get props => [
    id,
    reportDate,
    postedAt,
    netSalesRetail,
    salesVsGoalPercent,
    buysCount,
    buysTotal,
    hasDiscrepancy,
    laborPercent,
  ];
}
```

### 3.2 CloseReportDetail

File: `lib/domain/entities/close_reports/close_report_detail.dart`

```dart
class CloseReportDetail extends Equatable {
  final CloseReportMetadata metadata;
  final SalesSummary salesSummary;
  final BuysSummary buys;
  final ReturnsSummary returns;
  final LaborSummary labor;

  const CloseReportDetail({
    required this.metadata,
    required this.salesSummary,
    required this.buys,
    required this.returns,
    required this.labor,
  });

  @override
  List<Object?> get props => [metadata, salesSummary, buys, returns, labor];
}

class CloseReportMetadata extends Equatable {
  final int id;
  final DateTime reportDate;
  final DateTime postedAt;
  final String typeNum;

  const CloseReportMetadata({
    required this.id,
    required this.reportDate,
    required this.postedAt,
    required this.typeNum,
  });

  @override
  List<Object?> get props => [id, reportDate, postedAt, typeNum];
}

class SalesSummary extends Equatable {
  final double grossSalesRetail;
  final double grossSalesCost;
  final double grossSalesGM;
  final double netSalesRetail;
  final double netSalesCost;
  final double netSalesGM;
  final int salesCount;
  final double averageRetail;
  final double salesGoal;
  final double salesVsGoalPercent;

  const SalesSummary({
    required this.grossSalesRetail,
    required this.grossSalesCost,
    required this.grossSalesGM,
    required this.netSalesRetail,
    required this.netSalesCost,
    required this.netSalesGM,
    required this.salesCount,
    required this.averageRetail,
    required this.salesGoal,
    required this.salesVsGoalPercent,
  });

  // Formatted accessors
  String get netSalesFormatted => '\$${netSalesRetail.toStringAsFixed(2)}';
  String get goalFormatted => '\$${salesGoal.toStringAsFixed(2)}';
  String get avgRetailFormatted => '\$${averageRetail.toStringAsFixed(2)}';

  @override
  List<Object?> get props => [
    grossSalesRetail, grossSalesCost, grossSalesGM,
    netSalesRetail, netSalesCost, netSalesGM,
    salesCount, averageRetail, salesGoal, salesVsGoalPercent,
  ];
}

class BuysSummary extends Equatable {
  final double buysCost;
  final double buysRetail;
  final double buysGM;
  final int buysCount;
  final double buysGoal;
  final int buysOutstanding;
  final double buysVsGoalPercent;

  const BuysSummary({
    required this.buysCost,
    required this.buysRetail,
    required this.buysGM,
    required this.buysCount,
    required this.buysGoal,
    required this.buysOutstanding,
    required this.buysVsGoalPercent,
  });

  String get buysGoalFormatted => '\$${buysGoal.toStringAsFixed(2)}';

  @override
  List<Object?> get props => [
    buysCost, buysRetail, buysGM, buysCount, buysGoal, buysOutstanding, buysVsGoalPercent,
  ];
}

class ReturnsSummary extends Equatable {
  final double returnsCost;
  final double returnsRetail;
  final int returnsNumber;
  final double returnsGM;

  const ReturnsSummary({
    required this.returnsCost,
    required this.returnsRetail,
    required this.returnsNumber,
    required this.returnsGM,
  });

  @override
  List<Object?> get props => [returnsCost, returnsRetail, returnsNumber, returnsGM];
}

class LaborSummary extends Equatable {
  final double laborPercentage;
  final double laborHours;
  final double laborDollars;

  const LaborSummary({
    required this.laborPercentage,
    required this.laborHours,
    required this.laborDollars,
  });

  String get laborPercentFormatted => '${laborPercentage.toStringAsFixed(1)}%';
  String get laborHoursFormatted => laborHours.toStringAsFixed(1);
  String get laborDollarsFormatted => '\$${laborDollars.toStringAsFixed(2)}';

  @override
  List<Object?> get props => [laborPercentage, laborHours, laborDollars];
}
```

### 3.3 CloseReportComparison

File: `lib/domain/entities/close_reports/close_report_comparison.dart`

```dart
class CloseReportComparison extends Equatable {
  final CloseReportSummary primary;
  final CloseReportSummary? comparison;
  final ComparisonDeltas? deltas;
  final List<ComparisonHighlight> highlights;

  const CloseReportComparison({
    required this.primary,
    this.comparison,
    this.deltas,
    this.highlights = const [],
  });

  bool get hasComparison => comparison != null;

  @override
  List<Object?> get props => [primary, comparison, deltas, highlights];
}

class ComparisonDeltas extends Equatable {
  final double netSalesRetail;
  final double netSalesRetailPercent;
  final int buysCount;
  final double buysCountPercent;
  final double buysTotal;
  final double buysTotalPercent;
  final double salesVsGoalPercent;
  final double laborPercent;

  const ComparisonDeltas({
    required this.netSalesRetail,
    required this.netSalesRetailPercent,
    required this.buysCount,
    required this.buysCountPercent,
    required this.buysTotal,
    required this.buysTotalPercent,
    required this.salesVsGoalPercent,
    required this.laborPercent,
  });

  @override
  List<Object?> get props => [
    netSalesRetail, netSalesRetailPercent, buysCount, buysCountPercent,
    buysTotal, buysTotalPercent, salesVsGoalPercent, laborPercent,
  ];
}

enum VarianceSignificance { warning, critical }

enum VarianceDirection { up, down }

class ComparisonHighlight extends Equatable {
  final String field;
  final String label;
  final VarianceDirection direction;
  final double percentChange;
  final VarianceSignificance significance;

  const ComparisonHighlight({
    required this.field,
    required this.label,
    required this.direction,
    required this.percentChange,
    required this.significance,
  });

  bool get isPositive => direction == VarianceDirection.up;
  bool get isCritical => significance == VarianceSignificance.critical;

  @override
  List<Object?> get props => [field, label, direction, percentChange, significance];
}
```

### 3.4 CalendarDates

File: `lib/domain/entities/close_reports/calendar_dates.dart`

```dart
class CalendarDates extends Equatable {
  final Set<DateTime> dates;
  final int year;
  final int? month;

  const CalendarDates({
    required this.dates,
    required this.year,
    this.month,
  });

  bool hasReportForDate(DateTime date) {
    return dates.any((d) =>
      d.year == date.year && d.month == date.month && d.day == date.day
    );
  }

  @override
  List<Object?> get props => [dates, year, month];
}
```

### 3.5 Barrel Export

File: `lib/domain/entities/close_reports/close_report_entities.dart`

```dart
export 'close_report_summary.dart';
export 'close_report_detail.dart';
export 'close_report_comparison.dart';
export 'calendar_dates.dart';
```

---

## 4. Mappers

Mappers convert between data models and domain entities, handling the cents-to-dollars conversion.

### 4.1 CloseReportMapper

File: `lib/data/models/mappers/close_reports/close_report_mapper.dart`

```dart
/// Utility to convert cents (int) to dollars (double)
double _centsToDollars(int cents) => cents / 100.0;

/// Utility to parse date strings to DateTime
DateTime _parseDateTime(String dateStr) => DateTime.parse(dateStr);
DateTime _parseDate(String dateStr) {
  // YYYY-MM-DD format
  final parts = dateStr.split('-');
  return DateTime(int.parse(parts[0]), int.parse(parts[1]), int.parse(parts[2]));
}

extension CloseReportSummaryModelX on CloseReportSummaryModel {
  CloseReportSummary toEntity() {
    return CloseReportSummary(
      id: id,
      reportDate: _parseDate(reportDate),
      postedAt: _parseDateTime(postedAt),
      netSalesRetail: _centsToDollars(netSalesRetail),
      salesVsGoalPercent: salesVsGoalPercent,
      buysCount: buysCount,
      buysTotal: _centsToDollars(buysTotal),
      hasDiscrepancy: hasDiscrepancy,
      laborPercent: laborPercent,
    );
  }
}

extension CloseReportDetailModelX on CloseReportDetailModel {
  CloseReportDetail toEntity() {
    return CloseReportDetail(
      metadata: metadata.toEntity(),
      salesSummary: salesSummary.toEntity(),
      buys: buys.toEntity(),
      returns: returns.toEntity(),
      labor: labor.toEntity(),
    );
  }
}

extension CloseReportMetadataModelX on CloseReportMetadataModel {
  CloseReportMetadata toEntity() {
    return CloseReportMetadata(
      id: id,
      reportDate: _parseDate(reportDate),
      postedAt: _parseDateTime(postedAt),
      typeNum: typeNum,
    );
  }
}

extension SalesSummaryModelX on SalesSummaryModel {
  SalesSummary toEntity() {
    return SalesSummary(
      grossSalesRetail: _centsToDollars(grossSalesRetail),
      grossSalesCost: _centsToDollars(grossSalesCost),
      grossSalesGM: _centsToDollars(grossSalesGM),
      netSalesRetail: _centsToDollars(netSalesRetail),
      netSalesCost: _centsToDollars(netSalesCost),
      netSalesGM: _centsToDollars(netSalesGM),
      salesCount: salesCount,
      averageRetail: _centsToDollars(averageRetail),
      salesGoal: _centsToDollars(salesGoal),
      salesVsGoalPercent: salesVsGoalPercent,
    );
  }
}

extension BuysSummaryModelX on BuysSummaryModel {
  BuysSummary toEntity() {
    return BuysSummary(
      buysCost: _centsToDollars(buysCost),
      buysRetail: _centsToDollars(buysRetail),
      buysGM: _centsToDollars(buysGM),
      buysCount: buysCount,
      buysGoal: _centsToDollars(buysGoal),
      buysOutstanding: buysOutstanding,
      buysVsGoalPercent: buysVsGoalPercent,
    );
  }
}

extension ReturnsSummaryModelX on ReturnsSummaryModel {
  ReturnsSummary toEntity() {
    return ReturnsSummary(
      returnsCost: _centsToDollars(returnsCost),
      returnsRetail: _centsToDollars(returnsRetail),
      returnsNumber: returnsNumber,
      returnsGM: _centsToDollars(returnsGM),
    );
  }
}

extension LaborSummaryModelX on LaborSummaryModel {
  LaborSummary toEntity() {
    return LaborSummary(
      laborPercentage: laborPercentage,
      laborHours: laborHours,
      laborDollars: _centsToDollars(laborDollars),
    );
  }
}

extension CloseReportComparisonModelX on CloseReportComparisonModel {
  CloseReportComparison toEntity() {
    return CloseReportComparison(
      primary: primary.toEntity(),
      comparison: comparison?.toEntity(),
      deltas: deltas?.toEntity(),
      highlights: highlights.map((h) => h.toEntity()).toList(),
    );
  }
}

extension ComparisonDeltasModelX on ComparisonDeltasModel {
  ComparisonDeltas toEntity() {
    return ComparisonDeltas(
      netSalesRetail: _centsToDollars(netSalesRetail),
      netSalesRetailPercent: netSalesRetailPercent,
      buysCount: buysCount,
      buysCountPercent: buysCountPercent,
      buysTotal: _centsToDollars(buysTotal),
      buysTotalPercent: buysTotalPercent,
      salesVsGoalPercent: salesVsGoalPercent,
      laborPercent: laborPercent,
    );
  }
}

extension ComparisonHighlightModelX on ComparisonHighlightModel {
  ComparisonHighlight toEntity() {
    return ComparisonHighlight(
      field: field,
      label: label,
      direction: direction == 'up' ? VarianceDirection.up : VarianceDirection.down,
      percentChange: percentChange,
      significance: significance == 'critical'
          ? VarianceSignificance.critical
          : VarianceSignificance.warning,
    );
  }
}

extension CalendarDatesModelX on CalendarDatesModel {
  CalendarDates toEntity() {
    return CalendarDates(
      dates: dates.map(_parseDate).toSet(),
      year: year,
      month: month,
    );
  }
}
```

---

## 5. Repository Interface

### 5.1 CloseReportsRepository Interface

File: `lib/domain/repositories/close_reports_repository.dart`

```dart
/// Repository interface for close reports operations.
abstract class CloseReportsRepository {
  /// Gets a paginated list of close reports for a store.
  ///
  /// [typeNum] - Store identifier
  /// [limit] - Maximum number of reports to return (default: 30, max: 100)
  /// [offset] - Number of reports to skip for pagination
  ///
  /// Returns a tuple of (reports list, hasMore boolean).
  Future<(List<CloseReportSummary>, bool hasMore)> getReportsList({
    required String typeNum,
    int limit = 30,
    int offset = 0,
  });

  /// Gets the full detail for a specific report date.
  ///
  /// [typeNum] - Store identifier
  /// [date] - Report date in YYYY-MM-DD format
  ///
  /// Throws [NotFoundException] if no report exists for the date.
  Future<CloseReportDetail> getReportDetail({
    required String typeNum,
    required String date,
  });

  /// Gets the most recent close report for a store.
  ///
  /// [typeNum] - Store identifier
  ///
  /// Throws [NotFoundException] if no reports exist for the store.
  Future<CloseReportDetail> getLatestReport({
    required String typeNum,
  });

  /// Gets dates with available reports for calendar display.
  ///
  /// [typeNum] - Store identifier
  /// [year] - Year to query
  /// [month] - Optional month (1-12) to filter to single month
  ///
  /// Returns CalendarDates with set of available dates.
  Future<CalendarDates> getCalendarDates({
    required String typeNum,
    required int year,
    int? month,
  });

  /// Compares two reports with delta calculations.
  ///
  /// [typeNum] - Store identifier
  /// [primaryDate] - Primary (typically newer) report date
  /// [comparisonDate] - Comparison (typically older) report date
  ///
  /// Returns comparison with deltas and variance highlights.
  /// If comparison report doesn't exist, comparison/deltas will be null.
  Future<CloseReportComparison> compareReports({
    required String typeNum,
    required String primaryDate,
    required String comparisonDate,
  });
}
```

---

## 6. Repository Implementation

### 6.1 CloseReportsRepositoryImpl

File: `lib/data/repositories/close_reports_repository_impl.dart`

```dart
class CloseReportsRepositoryImpl implements CloseReportsRepository {
  final CloseReportsRemoteDatasource _remoteDatasource;

  CloseReportsRepositoryImpl({required CloseReportsRemoteDatasource remoteDatasource})
      : _remoteDatasource = remoteDatasource;

  @override
  Future<(List<CloseReportSummary>, bool)> getReportsList({
    required String typeNum,
    int limit = 30,
    int offset = 0,
  }) async {
    final response = await _remoteDatasource.getReportsList(
      typeNum: typeNum,
      limit: limit,
      offset: offset,
    );
    final reports = response.reports.map((m) => m.toEntity()).toList();
    return (reports, response.pagination.hasMore);
  }

  @override
  Future<CloseReportDetail> getReportDetail({
    required String typeNum,
    required String date,
  }) async {
    final model = await _remoteDatasource.getReportDetail(
      typeNum: typeNum,
      date: date,
    );
    return model.toEntity();
  }

  @override
  Future<CloseReportDetail> getLatestReport({
    required String typeNum,
  }) async {
    final model = await _remoteDatasource.getLatestReport(typeNum: typeNum);
    return model.toEntity();
  }

  @override
  Future<CalendarDates> getCalendarDates({
    required String typeNum,
    required int year,
    int? month,
  }) async {
    final model = await _remoteDatasource.getCalendarDates(
      typeNum: typeNum,
      year: year,
      month: month,
    );
    return model.toEntity();
  }

  @override
  Future<CloseReportComparison> compareReports({
    required String typeNum,
    required String primaryDate,
    required String comparisonDate,
  }) async {
    final model = await _remoteDatasource.compareReports(
      typeNum: typeNum,
      primaryDate: primaryDate,
      comparisonDate: comparisonDate,
    );
    return model.toEntity();
  }
}
```

---

## 7. Remote Data Source

### 7.1 CloseReportsRemoteDatasource Interface

File: `lib/data/datasources/close_reports/close_reports_remote_datasource.dart`

```dart
/// Abstract interface for close reports remote data operations
abstract class CloseReportsRemoteDatasource {
  Future<CloseReportListResponseModel> getReportsList({
    required String typeNum,
    int limit = 30,
    int offset = 0,
  });

  Future<CloseReportDetailModel> getReportDetail({
    required String typeNum,
    required String date,
  });

  Future<CloseReportDetailModel> getLatestReport({
    required String typeNum,
  });

  Future<CalendarDatesModel> getCalendarDates({
    required String typeNum,
    required int year,
    int? month,
  });

  Future<CloseReportComparisonModel> compareReports({
    required String typeNum,
    required String primaryDate,
    required String comparisonDate,
  });
}

/// Implementation using SchedulingApiClient (shared JWT auth)
///
/// **API Base URL Handling**: The `SchedulingApiClient` is configured with the
/// base URL (e.g., `https://api.buyerkiosk.com/api/mobile`). This datasource
/// constructs full paths by appending the close-reports path segments.
///
/// Full URL pattern: `${baseUrl}/close-reports/$typeNum/endpoint`
///
/// The Dio client is configured with the same base as the scheduling module,
/// sharing JWT authentication handling.
class CloseReportsRemoteDatasourceImpl implements CloseReportsRemoteDatasource {
  final SchedulingApiClient _apiClient;

  /// Base path for close reports API (appended to client's base URL)
  static const String _basePath = '/close-reports';

  CloseReportsRemoteDatasourceImpl({required SchedulingApiClient apiClient})
      : _apiClient = apiClient;

  /// Helper to handle API errors consistently
  Never _handleError(DioException e) {
    final statusCode = e.response?.statusCode;
    final data = e.response?.data;
    final errorCode = data is Map ? data['error']?['code'] : null;
    final errorMessage = data is Map ? data['error']?['message'] : e.message;

    switch (statusCode) {
      case 400:
        throw ValidationException(errorMessage ?? 'Invalid request');
      case 401:
        throw AuthException(errorMessage ?? 'Unauthorized');
      case 403:
        throw AuthException(errorMessage ?? 'Access denied');
      case 404:
        throw NotFoundException(errorMessage ?? 'Report not found');
      case 503:
        throw ServiceUnavailableException(
          errorMessage ?? 'Close reports service unavailable',
        );
      default:
        if (e.type == DioExceptionType.connectionTimeout ||
            e.type == DioExceptionType.receiveTimeout ||
            e.type == DioExceptionType.connectionError) {
          throw NetworkException('Unable to connect to server');
        }
        throw ServerException(errorMessage ?? 'Server error');
    }
  }

  @override
  Future<CloseReportListResponseModel> getReportsList({
    required String typeNum,
    int limit = 30,
    int offset = 0,
  }) async {
    try {
      final response = await _apiClient.post(
        '$_basePath/$typeNum/list',
        data: {'limit': limit, 'offset': offset},
      );
      return CloseReportListResponseModel.fromJson(
        response.data as Map<String, dynamic>,
      );
    } on DioException catch (e) {
      _handleError(e);
    }
  }

  @override
  Future<CloseReportDetailModel> getReportDetail({
    required String typeNum,
    required String date,
  }) async {
    try {
      final response = await _apiClient.post(
        '$_basePath/$typeNum/detail',
        data: {'date': date},
      );
      final data = response.data as Map<String, dynamic>;
      return CloseReportDetailModel.fromJson(data['report'] as Map<String, dynamic>);
    } on DioException catch (e) {
      _handleError(e);
    }
  }

  @override
  Future<CloseReportDetailModel> getLatestReport({
    required String typeNum,
  }) async {
    try {
      final response = await _apiClient.post(
        '$_basePath/$typeNum/latest',
        data: {},
      );
      final data = response.data as Map<String, dynamic>;
      return CloseReportDetailModel.fromJson(data['report'] as Map<String, dynamic>);
    } on DioException catch (e) {
      _handleError(e);
    }
  }

  @override
  Future<CalendarDatesModel> getCalendarDates({
    required String typeNum,
    required int year,
    int? month,
  }) async {
    try {
      final response = await _apiClient.post(
        '$_basePath/$typeNum/calendar',
        data: {
          'year': year,
          if (month != null) 'month': month,
        },
      );
      return CalendarDatesModel.fromJson(response.data as Map<String, dynamic>);
    } on DioException catch (e) {
      _handleError(e);
    }
  }

  @override
  Future<CloseReportComparisonModel> compareReports({
    required String typeNum,
    required String primaryDate,
    required String comparisonDate,
  }) async {
    try {
      final response = await _apiClient.post(
        '$_basePath/$typeNum/compare',
        data: {
          'primaryDate': primaryDate,
          'comparisonDate': comparisonDate,
        },
      );
      return CloseReportComparisonModel.fromJson(
        response.data as Map<String, dynamic>,
      );
    } on DioException catch (e) {
      _handleError(e);
    }
  }
}
```

### 7.2 Barrel Export

File: `lib/data/datasources/close_reports/close_reports_datasources.dart`

```dart
export 'close_reports_remote_datasource.dart';
```

---

## 8. Providers (Riverpod 3.x)

### 8.1 Base Providers

File: `lib/presentation/providers/close_reports/close_reports_providers.dart`

```dart
// ============================================================================
// DATASOURCE & REPOSITORY PROVIDERS
// ============================================================================

/// Provides the close reports remote datasource
final closeReportsRemoteDatasourceProvider = Provider<CloseReportsRemoteDatasource>((ref) {
  final apiClient = ref.watch(schedulingApiClientProvider);
  return CloseReportsRemoteDatasourceImpl(apiClient: apiClient);
});

/// Provides the close reports repository
final closeReportsRepositoryProvider = Provider<CloseReportsRepository>((ref) {
  final remoteDatasource = ref.watch(closeReportsRemoteDatasourceProvider);
  return CloseReportsRepositoryImpl(remoteDatasource: remoteDatasource);
});

// ============================================================================
// REPORT LIST PROVIDER
// ============================================================================

/// State for paginated report list
class CloseReportsListState extends Equatable {
  final List<CloseReportSummary> reports;
  final bool hasMore;
  final bool isLoadingMore;

  const CloseReportsListState({
    this.reports = const [],
    this.hasMore = true,
    this.isLoadingMore = false,
  });

  CloseReportsListState copyWith({
    List<CloseReportSummary>? reports,
    bool? hasMore,
    bool? isLoadingMore,
  }) {
    return CloseReportsListState(
      reports: reports ?? this.reports,
      hasMore: hasMore ?? this.hasMore,
      isLoadingMore: isLoadingMore ?? this.isLoadingMore,
    );
  }

  @override
  List<Object?> get props => [reports, hasMore, isLoadingMore];
}

/// Notifier for paginated close reports list
class CloseReportsListNotifier extends FamilyAsyncNotifier<CloseReportsListState, String> {
  static const int _pageSize = 30;

  @override
  Future<CloseReportsListState> build(String typeNum) async {
    final repository = ref.read(closeReportsRepositoryProvider);
    final (reports, hasMore) = await repository.getReportsList(
      typeNum: typeNum,
      limit: _pageSize,
      offset: 0,
    );
    return CloseReportsListState(reports: reports, hasMore: hasMore);
  }

  Future<void> loadMore() async {
    final currentState = state.valueOrNull;
    if (currentState == null || !currentState.hasMore || currentState.isLoadingMore) {
      return;
    }

    state = AsyncData(currentState.copyWith(isLoadingMore: true));

    try {
      final repository = ref.read(closeReportsRepositoryProvider);
      final (newReports, hasMore) = await repository.getReportsList(
        typeNum: arg,
        limit: _pageSize,
        offset: currentState.reports.length,
      );

      state = AsyncData(CloseReportsListState(
        reports: [...currentState.reports, ...newReports],
        hasMore: hasMore,
        isLoadingMore: false,
      ));
    } catch (e, st) {
      state = AsyncData(currentState.copyWith(isLoadingMore: false));
      // UI should show toast/snackbar: "Failed to load more reports. Tap to retry."
      // The error is captured but state is restored so user can retry
    }
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => build(arg));
  }
}

/// Provider for paginated close reports list
final closeReportsListProvider = AsyncNotifierProvider.family<
    CloseReportsListNotifier, CloseReportsListState, String>(
  CloseReportsListNotifier.new,
);

// ============================================================================
// REPORT DETAIL PROVIDER
// ============================================================================

/// Parameters for report detail provider
class ReportDetailParams extends Equatable {
  final String typeNum;
  final String date;

  const ReportDetailParams({required this.typeNum, required this.date});

  @override
  List<Object?> get props => [typeNum, date];
}

/// Notifier for close report detail
class CloseReportDetailNotifier extends FamilyAsyncNotifier<CloseReportDetail, ReportDetailParams> {
  @override
  Future<CloseReportDetail> build(ReportDetailParams params) async {
    final repository = ref.read(closeReportsRepositoryProvider);
    return await repository.getReportDetail(
      typeNum: params.typeNum,
      date: params.date,
    );
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => build(arg));
  }
}

/// Provider for close report detail by typeNum and date
final closeReportDetailProvider = AsyncNotifierProvider.family<
    CloseReportDetailNotifier, CloseReportDetail, ReportDetailParams>(
  CloseReportDetailNotifier.new,
);

// ============================================================================
// LATEST REPORT PROVIDER
// ============================================================================

/// Notifier for latest close report
class LatestCloseReportNotifier extends FamilyAsyncNotifier<CloseReportDetail, String> {
  @override
  Future<CloseReportDetail> build(String typeNum) async {
    final repository = ref.read(closeReportsRepositoryProvider);
    return await repository.getLatestReport(typeNum: typeNum);
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => build(arg));
  }
}

/// Provider for latest close report
final latestCloseReportProvider = AsyncNotifierProvider.family<
    LatestCloseReportNotifier, CloseReportDetail, String>(
  LatestCloseReportNotifier.new,
);

// ============================================================================
// CALENDAR PROVIDER
// ============================================================================

/// Parameters for calendar dates provider
class CalendarParams extends Equatable {
  final String typeNum;
  final int year;
  final int? month;

  const CalendarParams({required this.typeNum, required this.year, this.month});

  @override
  List<Object?> get props => [typeNum, year, month];
}

/// Notifier for calendar dates
class CloseReportCalendarNotifier extends FamilyAsyncNotifier<CalendarDates, CalendarParams> {
  @override
  Future<CalendarDates> build(CalendarParams params) async {
    final repository = ref.read(closeReportsRepositoryProvider);
    return await repository.getCalendarDates(
      typeNum: params.typeNum,
      year: params.year,
      month: params.month,
    );
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => build(arg));
  }
}

/// Provider for calendar dates
final closeReportCalendarProvider = AsyncNotifierProvider.family<
    CloseReportCalendarNotifier, CalendarDates, CalendarParams>(
  CloseReportCalendarNotifier.new,
);

// ============================================================================
// COMPARISON PROVIDER
// ============================================================================

/// Parameters for comparison provider
class ComparisonParams extends Equatable {
  final String typeNum;
  final String primaryDate;
  final String comparisonDate;

  const ComparisonParams({
    required this.typeNum,
    required this.primaryDate,
    required this.comparisonDate,
  });

  @override
  List<Object?> get props => [typeNum, primaryDate, comparisonDate];
}

/// Notifier for report comparison
class CloseReportComparisonNotifier extends FamilyAsyncNotifier<CloseReportComparison, ComparisonParams> {
  @override
  Future<CloseReportComparison> build(ComparisonParams params) async {
    final repository = ref.read(closeReportsRepositoryProvider);
    return await repository.compareReports(
      typeNum: params.typeNum,
      primaryDate: params.primaryDate,
      comparisonDate: params.comparisonDate,
    );
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => build(arg));
  }
}

/// Provider for report comparison
final closeReportComparisonProvider = AsyncNotifierProvider.family<
    CloseReportComparisonNotifier, CloseReportComparison, ComparisonParams>(
  CloseReportComparisonNotifier.new,
);

// ============================================================================
// UI STATE PROVIDERS
// ============================================================================

/// Selected date for detail view (session-scoped)
final selectedReportDateProvider = StateProvider.family<String?, String>((ref, typeNum) => null);

/// Section expansion states (session-scoped)
final reportSectionExpandedProvider = StateProvider.family<Map<String, bool>, String>(
  (ref, typeNum) => {
    'salesSummary': true,
    'buys': false,
    'returns': false,
    'labor': false,
  },
);
```

### 8.2 Barrel Export Update

Add to `lib/presentation/providers/providers.dart`:

```dart
// Close Reports providers
export 'close_reports/close_reports_providers.dart';
```

---

## 9. Navigation & Permissions

### 9.1 Add AppPage Enum Entry

File: `lib/core/constants/permission_constants.dart`

Add to `AppPage` enum:

```dart
closeReports('Close Reports', '/store/:typeNum/close-reports'),
```

Add to `DefaultPermissions.defaults`:

```dart
AppPage.closeReports: AccessLevel.shiftLead,
```

### 9.2 Router Updates

File: `lib/router/app_router.dart`

Add route under store detail routes:

```dart
GoRoute(
  path: 'close-reports',
  name: 'store-close-reports',
  builder: (context, state) {
    final typeNum = state.pathParameters['typeNum']!;
    final storeName = state.uri.queryParameters['storeName'];
    return CloseReportsScreen(
      typeNum: typeNum,
      storeName: storeName,
    );
  },
  routes: [
    GoRoute(
      path: ':date',
      name: 'store-close-report-detail',
      builder: (context, state) {
        final typeNum = state.pathParameters['typeNum']!;
        final date = state.pathParameters['date']!;
        final storeName = state.uri.queryParameters['storeName'];
        return CloseReportDetailScreen(
          typeNum: typeNum,
          date: date,
          storeName: storeName,
        );
      },
    ),
    GoRoute(
      path: 'compare',
      name: 'store-close-report-compare',
      builder: (context, state) {
        final typeNum = state.pathParameters['typeNum']!;
        final primaryDate = state.uri.queryParameters['primary']!;
        final comparisonDate = state.uri.queryParameters['comparison']!;
        final storeName = state.uri.queryParameters['storeName'];
        return CloseReportComparisonScreen(
          typeNum: typeNum,
          primaryDate: primaryDate,
          comparisonDate: comparisonDate,
          storeName: storeName,
        );
      },
    ),
  ],
),
```

Add helper to `_getAppPageFromLocation`:

```dart
if (path.contains('/close-reports')) return AppPage.closeReports;
```

Add navigation extension:

```dart
/// Navigate to close reports
void goToCloseReports(String typeNum, {String? storeName}) {
  final uri = Uri(
    path: '/store/$typeNum/close-reports',
    queryParameters: storeName != null ? {'storeName': storeName} : null,
  );
  go(uri.toString());
}

/// Navigate to close report detail
void goToCloseReportDetail(String typeNum, String date, {String? storeName}) {
  final uri = Uri(
    path: '/store/$typeNum/close-reports/$date',
    queryParameters: storeName != null ? {'storeName': storeName} : null,
  );
  go(uri.toString());
}

/// Navigate to close report comparison
void goToCloseReportComparison(
  String typeNum,
  String primaryDate,
  String comparisonDate, {
  String? storeName,
}) {
  final params = <String, String>{
    'primary': primaryDate,
    'comparison': comparisonDate,
    if (storeName != null) 'storeName': storeName,
  };
  final uri = Uri(
    path: '/store/$typeNum/close-reports/compare',
    queryParameters: params,
  );
  go(uri.toString());
}
```

---

## 10. Screens

### 10.1 CloseReportsScreen

File: `lib/presentation/screens/close_reports/close_reports_screen.dart`

**Purpose**: Main entry point with tabs for Latest Report and Report History list.

**Component Hierarchy**:
```
CloseReportsScreen
├── AppBar
│   ├── BackButton
│   ├── Title (store name)
│   ├── CalendarButton (opens date picker)
│   └── RefreshButton
├── TabBar
│   ├── Tab("Latest")
│   └── Tab("History")
└── TabBarView
    ├── LatestReportTab
    │   └── CloseReportDetailContent (reusable widget)
    └── ReportHistoryTab
        └── ReportListView (infinite scroll)
            └── CloseReportListTile (for each report)
```

**Key Behaviors**:
- Default to "Latest" tab on entry
- Calendar icon opens date picker with available dates highlighted
- Pull-to-refresh on both tabs
- Tapping list item navigates to detail screen for that date

**Empty State Handling**:
- Latest tab: "No close reports yet. Reports appear after your first store close." with empty state illustration
- History list: "No close reports found." with empty state illustration

### 10.2 CloseReportDetailScreen

File: `lib/presentation/screens/close_reports/close_report_detail_screen.dart`

**Purpose**: Full report view for a specific date with expandable sections.

**Component Hierarchy**:
```
CloseReportDetailScreen
├── AppBar
│   ├── BackButton
│   ├── Title (date formatted)
│   ├── CompareButton
│   └── RefreshButton
└── SingleChildScrollView
    ├── ReportHeader
    │   ├── PostedTimestamp
    │   ├── DiscrepancyBadge (if hasDiscrepancy - shows "Cash Discrepancy" label)
    │   └── KeyMetricsSummary
    │       ├── NetSalesCard
    │       ├── SalesVsGoalCard
    │       ├── BuysCountCard
    │       ├── BuysTotalCard
    │       └── LaborPercentCard
    ├── SalesSummarySection (expandable)
    │   └── SalesSummaryContent
    │       ├── GrossSalesRow
    │       ├── NetSalesRow
    │       ├── SalesCountRow
    │       ├── AvgRetailRow
    │       └── GoalProgressRow
    ├── BuysSection (expandable)
    │   └── BuysSummaryContent
    ├── ReturnsSection (expandable)
    │   └── ReturnsSummaryContent
    └── LaborSection (expandable)
        └── LaborSummaryContent
```

**Key Behaviors**:
- Sections expand/collapse with 200-300ms animation
- Section expansion state persists within session
- Compare button opens date selector modal
- Discrepancy highlighted with warning styling (displays "Cash Discrepancy" label when `hasDiscrepancy: true`)
- QuickCompareButtons ("vs Yesterday", "vs Last Week") shown between AppBar and main content, or in a bottom sheet when "Compare" is tapped
  - "vs Yesterday" compares to the previous day's report
  - "vs Last Week" compares to the same weekday from the previous week

**API Limitation Note**: The backend API only provides `hasDiscrepancy: boolean` - no discrepancy type details are available. The UI displays a generic "Cash Discrepancy" label when `hasDiscrepancy` is true.

### 10.3 CloseReportComparisonScreen

File: `lib/presentation/screens/close_reports/close_report_comparison_screen.dart`

**Purpose**: Side-by-side comparison of two reports with delta highlighting.

**Component Hierarchy**:
```
CloseReportComparisonScreen
├── AppBar
│   ├── BackButton
│   ├── Title ("Compare Reports")
│   ├── SwapButton (swap primary/comparison)
│   └── ChangeDateButton
└── SingleChildScrollView
    ├── ComparisonHeader
    │   ├── PrimaryDateLabel
    │   ├── Arrow
    │   └── ComparisonDateLabel
    ├── HighlightsBanner (if significant variances)
    │   └── List<HighlightChip>
    └── ComparisonTable
        ├── MetricRow("Net Sales", primary, comparison, delta)
        ├── MetricRow("Sales vs Goal", primary, comparison, delta)
        ├── MetricRow("Buys Count", primary, comparison, delta)
        ├── MetricRow("Buys Total", primary, comparison, delta)
        └── MetricRow("Labor %", primary, comparison, delta)
```

**Key Behaviors**:
- Delta values colored: green=positive, red=negative
- Labor % inverted: lower is better (negative delta = green)
- Variance highlights: >10% = warning (amber), >25% = critical (red)
- Swap button reverses primary/comparison
- Change date opens date selector for comparison date

### 10.4 Barrel Export

File: `lib/presentation/screens/close_reports/close_reports_screens.dart`

```dart
export 'close_reports_screen.dart';
export 'close_report_detail_screen.dart';
export 'close_report_comparison_screen.dart';
```

---

## 11. Widgets

### 11.1 Common Widgets

File: `lib/presentation/widgets/close_reports/`

| Widget | Purpose |
|--------|---------|
| `CloseReportListTile` | List item showing report summary with discrepancy indicator |
| `MetricRow` | Reusable row showing metric label, value, optional delta |
| `ExpandableSection` | Collapsible section with animated expand/collapse |
| `DiscrepancyBadge` | Warning badge for reports with discrepancies (displays "Cash Discrepancy" when `hasDiscrepancy: true`) |
| `DateSelectorModal` | Calendar modal with available dates highlighted |
| `QuickCompareButtons` | "vs Yesterday", "vs Last Week" shortcut buttons |
| `ComparisonMetricRow` | Three-column row: primary, comparison, delta |
| `VarianceChip` | Colored chip showing variance significance |

### 11.2 DateSelectorModal Behavior

The `DateSelectorModal` calendar widget has the following specified behaviors:

- **Default View**: Opens to the current month
- **Available Dates**: Dates with reports are highlighted with a dot indicator below the date number
- **Disabled Dates**: Dates without reports are greyed out and not tappable
- **Month Navigation**: Previous/next month navigation arrows in the header
- **Date Selection**: Tapping an available date loads that report and dismisses the modal
- **Loading State**: Shows loading indicator while fetching calendar dates from API

### 11.3 Barrel Export

File: `lib/presentation/widgets/close_reports/close_reports_widgets.dart`

```dart
export 'close_report_list_tile.dart';
export 'metric_row.dart';
export 'expandable_section.dart';
export 'discrepancy_badge.dart';
export 'date_selector_modal.dart';
export 'quick_compare_buttons.dart';
export 'comparison_metric_row.dart';
export 'variance_chip.dart';
```

---

## 12. Error Handling

### 12.1 Error Types

| Error | HTTP Status | User Message | Recovery |
|-------|-------------|--------------|----------|
| `NotFoundException` | 404 | "No report found for this date" | Show toast, return to list |
| `AuthException` | 401/403 | "Session expired. Please log in again." | Redirect to login |
| `NetworkException` | timeout/connection | "Unable to connect. Check your connection." | Retry button |
| `ValidationException` | 400 | "Invalid date format" | Show form validation error |
| `ServiceUnavailableException` | 503 | "Close reports are temporarily unavailable" | Retry button |
| `ServerException` | 500 | "Something went wrong. Please try again." | Retry button |

### 12.2 Calendar/Date Selection Errors

Specific toast messages for calendar and date navigation errors:

| Scenario | Toast Message |
|----------|---------------|
| Failed to load report for selected date | "Failed to load report for [date]. Try again." |
| Failed to load calendar data | "Failed to load calendar. Check your connection." |
| Selected date has no report | "No report available for [date]." |

### 12.3 Error Display

Use existing `ErrorDisplay.fromError()` widget pattern:

```dart
todayPerformanceState.when(
  data: (data) => _buildContent(data),
  loading: () => const LoadingIndicator(message: 'Loading report...'),
  error: (error, stackTrace) => ErrorDisplay.fromError(
    error: error,
    onRetry: () => ref.read(provider.notifier).refresh(),
  ),
)
```

---

## 13. File Structure Summary

```
lib/
├── core/
│   └── constants/
│       └── permission_constants.dart  # Add AppPage.closeReports
├── data/
│   ├── datasources/
│   │   └── close_reports/
│   │       ├── close_reports_remote_datasource.dart
│   │       └── close_reports_datasources.dart
│   ├── models/
│   │   ├── close_reports/
│   │   │   ├── close_report_summary_model.dart
│   │   │   ├── close_report_detail_model.dart
│   │   │   ├── close_report_comparison_model.dart
│   │   │   ├── calendar_dates_model.dart
│   │   │   ├── close_report_list_response_model.dart
│   │   │   └── close_report_models.dart
│   │   └── mappers/
│   │       └── close_reports/
│   │           └── close_report_mapper.dart
│   └── repositories/
│       └── close_reports_repository_impl.dart
├── domain/
│   ├── entities/
│   │   └── close_reports/
│   │       ├── close_report_summary.dart
│   │       ├── close_report_detail.dart
│   │       ├── close_report_comparison.dart
│   │       ├── calendar_dates.dart
│   │       └── close_report_entities.dart
│   └── repositories/
│       └── close_reports_repository.dart
├── presentation/
│   ├── providers/
│   │   ├── close_reports/
│   │   │   └── close_reports_providers.dart
│   │   └── providers.dart  # Add export
│   ├── screens/
│   │   └── close_reports/
│   │       ├── close_reports_screen.dart
│   │       ├── close_report_detail_screen.dart
│   │       ├── close_report_comparison_screen.dart
│   │       └── close_reports_screens.dart
│   └── widgets/
│       └── close_reports/
│           ├── close_report_list_tile.dart
│           ├── metric_row.dart
│           ├── expandable_section.dart
│           ├── discrepancy_badge.dart
│           ├── date_selector_modal.dart
│           ├── quick_compare_buttons.dart
│           ├── comparison_metric_row.dart
│           ├── variance_chip.dart
│           └── close_reports_widgets.dart
└── router/
    └── app_router.dart  # Add routes and navigation helpers
```

---

## 14. API Integration Details

### 14.1 Base URL

The Close Reports API uses the same base as scheduling:
- **Dev**: `https://api.buyerkiosk.com/api/mobile/close-reports`
- Uses JWT Bearer token (same as scheduling)
- Reuses `SchedulingApiClient` to share auth interceptor

### 14.2 Endpoint Summary

| Provider | Endpoint | HTTP |
|----------|----------|------|
| `closeReportsListProvider` | `/:typeNum/list` | POST |
| `closeReportDetailProvider` | `/:typeNum/detail` | POST |
| `latestCloseReportProvider` | `/:typeNum/latest` | POST |
| `closeReportCalendarProvider` | `/:typeNum/calendar` | POST |
| `closeReportComparisonProvider` | `/:typeNum/compare` | POST |

---

## 15. Testing Strategy

### 15.1 Unit Tests

| Test File | Coverage |
|-----------|----------|
| `test/data/models/close_reports/close_report_models_test.dart` | Model JSON parsing |
| `test/data/models/mappers/close_reports/close_report_mapper_test.dart` | Mapper conversions, cents-to-dollars |
| `test/data/repositories/close_reports_repository_test.dart` | Repository methods |
| `test/presentation/providers/close_reports_providers_test.dart` | Provider state management |

### 15.2 Widget Tests

| Test File | Coverage |
|-----------|----------|
| `test/presentation/widgets/close_reports/close_report_widgets_test.dart` | Widget rendering |
| `test/presentation/screens/close_reports/close_reports_screen_test.dart` | Screen behavior |

### 15.3 Integration Tests

| Test File | Coverage |
|-----------|----------|
| `test/integration/close_reports_flow_test.dart` | Full user flow |

---

## 16. Architecture Decision Records

### ADR-1: Reuse SchedulingApiClient

**Decision**: Reuse existing `SchedulingApiClient` rather than creating separate close reports API client.

**Rationale**:
- Close Reports API uses same JWT authentication as scheduling
- Avoids duplicate auth interceptor code
- Simplifies token management
- API base URL is the same

**Trade-offs**:
- Close reports datasource depends on scheduling API client
- If scheduling module is removed, close reports would need refactoring

**Status**: Approved

### ADR-2: Cents-to-Dollars Conversion at Mapper Layer

**Decision**: Convert monetary values from cents (int) to dollars (double) in the mapper layer.

**Rationale**:
- Models match API response exactly (easier debugging)
- Entities use user-friendly dollar amounts
- Single conversion point prevents inconsistencies
- Consistent with how scheduling labor costs are handled

**Trade-offs**:
- Slight memory overhead (storing both int cents and double dollars)
- Must remember conversion happens at mapper layer

**Status**: Approved

### ADR-3: Family Providers with Parameter Objects

**Decision**: Use Equatable parameter objects for family providers with multiple parameters.

**Rationale**:
- Consistent with existing patterns (scheduling providers)
- Type-safe parameter passing
- Proper equality comparison for provider caching
- Cleaner than positional parameters

**Trade-offs**:
- More boilerplate code
- Must create parameter classes

**Status**: Approved

### ADR-4: Session-Scoped State Persistence

**Decision**: Section expansion states and selected dates persist only within app session, not across restarts.

**Rationale**:
- PRD specifies session-scoped persistence
- Simpler implementation (StateProvider vs persistent storage)
- Users expect fresh state on app restart
- Avoids stale date selections

**Trade-offs**:
- Users must re-expand sections after restart
- No persistence of "favorite" views

**Status**: Approved

---

## 17. Quality Requirements

### 17.1 Performance

| Requirement | Target | Measurement |
|-------------|--------|-------------|
| Report list load time | <2s on 3G | Time from tap to first item rendered |
| Report detail load time | <1.5s on 3G | Time from tap to content visible |
| Calendar load time | <1s | Time from tap to dates highlighted |
| Comparison load time | <2s | Time from date selection to comparison visible |

### 17.2 Accessibility

- All interactive elements have semantic labels
- Color is not the only indicator (icons accompany colors)
- Touch targets are at least 48x48dp
- Text scales with system font size

### 17.3 Reliability

- Retry button available on all error states
- Graceful degradation when comparison date unavailable
- Offline state shows clear "No connection" message

---

## 18. Implementation Order

**Phase 1: Data Layer**
1. Create Freezed models with JSON parsing
2. Create Equatable domain entities
3. Create mappers with cents-to-dollars conversion
4. Create remote datasource
5. Create repository interface and implementation
6. Add unit tests for models, mappers, repository

**Phase 2: Provider Layer**
1. Create base providers (datasource, repository)
2. Create list provider with pagination
3. Create detail and latest providers
4. Create calendar provider
5. Create comparison provider
6. Add UI state providers
7. Add provider tests

**Phase 3: Navigation**
1. Add AppPage.closeReports to enum
2. Add permission default
3. Add routes to router
4. Add navigation helpers
5. Add permission check to `_getAppPageFromLocation`

**Phase 4: UI Layer**
1. Create reusable widgets
2. Create CloseReportsScreen with tabs
3. Create CloseReportDetailScreen
4. Create CloseReportComparisonScreen
5. Add to store detail quick actions
6. Add widget and screen tests

**Phase 5: Integration**
1. Integration tests for full flow
2. Manual testing on device
3. Analytics event implementation

---

*SDD Version: 1.0.0*
*Created: 2026-01-26*
*Spec ID: 004-close-reports-frontend*
