feat: Introduce CoverageWorker entity and update coverage repository

- Added CoverageWorker entity to represent worker status and check-in information.
- Updated CoverageRepositoryImpl to utilize CoverageWorker and its status.
- Removed legacy coverage_entities.dart file and replaced references with krow_domain imports.
- Enhanced coverage statistics calculations based on new worker status logic.
- Updated UI components to reflect changes in worker status representation.
- Modified acceptance of shifts to align with new status definitions.
- Cleaned up QA testing checklist to remove outdated items and clarify requirements.
This commit is contained in:
Achintha Isuru
2026-02-01 21:05:14 -05:00
parent 109ed5375d
commit 08b96cea6f
15 changed files with 288 additions and 897 deletions

View File

@@ -87,6 +87,11 @@ export 'src/adapters/clock_in/clock_in_adapter.dart';
export 'src/entities/availability/availability_slot.dart';
export 'src/entities/availability/day_availability.dart';
// Coverage
export 'src/entities/coverage_domain/coverage_shift.dart';
export 'src/entities/coverage_domain/coverage_worker.dart';
export 'src/entities/coverage_domain/coverage_stats.dart';
// Adapters
export 'src/adapters/profile/emergency_contact_adapter.dart';
export 'src/adapters/profile/experience_adapter.dart';

View File

@@ -0,0 +1,57 @@
import 'package:equatable/equatable.dart';
import 'coverage_worker.dart';
/// Domain entity representing a shift in the coverage view.
///
/// This is a feature-specific domain entity that encapsulates shift information
/// including scheduling details and assigned workers.
class CoverageShift extends Equatable {
/// Creates a [CoverageShift].
const CoverageShift({
required this.id,
required this.title,
required this.location,
required this.startTime,
required this.workersNeeded,
required this.date,
required this.workers,
});
/// The unique identifier for the shift.
final String id;
/// The title or role of the shift.
final String title;
/// The location where the shift takes place.
final String location;
/// The start time of the shift (e.g., "16:00").
final String startTime;
/// The number of workers needed for this shift.
final int workersNeeded;
/// The date of the shift.
final DateTime date;
/// The list of workers assigned to this shift.
final List<CoverageWorker> workers;
/// Calculates the coverage percentage for this shift.
int get coveragePercent {
if (workersNeeded == 0) return 0;
return ((workers.length / workersNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
id,
title,
location,
startTime,
workersNeeded,
date,
workers,
];
}

View File

@@ -0,0 +1,45 @@
import 'package:equatable/equatable.dart';
/// Domain entity representing coverage statistics.
///
/// Aggregates coverage metrics for a specific date.
class CoverageStats extends Equatable {
/// Creates a [CoverageStats].
const CoverageStats({
required this.totalNeeded,
required this.totalConfirmed,
required this.checkedIn,
required this.enRoute,
required this.late,
});
/// The total number of workers needed.
final int totalNeeded;
/// The total number of confirmed workers.
final int totalConfirmed;
/// The number of workers who have checked in.
final int checkedIn;
/// The number of workers en route.
final int enRoute;
/// The number of late workers.
final int late;
/// Calculates the overall coverage percentage.
int get coveragePercent {
if (totalNeeded == 0) return 0;
return ((totalConfirmed / totalNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
totalNeeded,
totalConfirmed,
checkedIn,
enRoute,
late,
];
}

View File

@@ -0,0 +1,55 @@
import 'package:equatable/equatable.dart';
/// Worker status enum matching ApplicationStatus from Data Connect.
enum CoverageWorkerStatus {
/// Application is pending approval.
pending,
/// Application has been accepted.
accepted,
/// Application has been rejected.
rejected,
/// Worker has confirmed attendance.
confirmed,
/// Worker has checked in.
checkedIn,
/// Worker has checked out.
checkedOut,
/// Worker is late.
late,
/// Worker did not show up.
noShow,
/// Shift is completed.
completed,
}
/// Domain entity representing a worker in the coverage view.
///
/// This entity tracks worker status including check-in information.
class CoverageWorker extends Equatable {
/// Creates a [CoverageWorker].
const CoverageWorker({
required this.name,
required this.status,
this.checkInTime,
});
/// The name of the worker.
final String name;
/// The status of the worker.
final CoverageWorkerStatus status;
/// The time the worker checked in, if applicable.
final String? checkInTime;
@override
List<Object?> get props => <Object?>[name, status, checkInTime];
}

View File

@@ -1,7 +1,7 @@
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/coverage_repository.dart';
import '../../domain/ui_entities/coverage_entities.dart';
/// Implementation of [CoverageRepository] in the Data layer.
///
@@ -25,18 +25,14 @@ class CoverageRepositoryImpl implements CoverageRepository {
Future<List<CoverageShift>> getShiftsForDate({required DateTime date}) async {
final String? businessId =
dc.ClientSessionStore.instance.session?.business?.id;
print('Coverage: now=${DateTime.now().toIso8601String()}');
if (businessId == null || businessId.isEmpty) {
print('Coverage: missing businessId for date=${date.toIso8601String()}');
return <CoverageShift>[];
}
final DateTime start = DateTime(date.year, date.month, date.day);
final DateTime end =
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
print(
'Coverage: request businessId=$businessId dayStart=${start.toIso8601String()} dayEnd=${end.toIso8601String()}',
);
final fdc.QueryResult<
dc.ListShiftRolesByBusinessAndDateRangeData,
dc.ListShiftRolesByBusinessAndDateRangeVariables> shiftRolesResult =
@@ -58,9 +54,6 @@ class CoverageRepositoryImpl implements CoverageRepository {
dayEnd: _toTimestamp(end),
)
.execute();
print(
'Coverage: ${date.toIso8601String()} staffsApplications=${applicationsResult.data.applications.length}',
);
return _mapCoverageShifts(
shiftRolesResult.data.shiftRoles,
@@ -84,11 +77,16 @@ class CoverageRepositoryImpl implements CoverageRepository {
final List<CoverageWorker> allWorkers =
shifts.expand((CoverageShift shift) => shift.workers).toList();
final int totalConfirmed = allWorkers.length;
final int checkedIn =
allWorkers.where((CoverageWorker w) => w.isCheckedIn).length;
final int enRoute =
allWorkers.where((CoverageWorker w) => w.isEnRoute).length;
final int late = allWorkers.where((CoverageWorker w) => w.isLate).length;
final int checkedIn = allWorkers
.where((CoverageWorker w) => w.status == CoverageWorkerStatus.checkedIn)
.length;
final int enRoute = allWorkers
.where((CoverageWorker w) =>
w.status == CoverageWorkerStatus.confirmed && w.checkInTime == null)
.length;
final int late = allWorkers
.where((CoverageWorker w) => w.status == CoverageWorkerStatus.late)
.length;
return CoverageStats(
totalNeeded: totalNeeded,
@@ -172,25 +170,32 @@ class CoverageRepositoryImpl implements CoverageRepository {
.toList();
}
String _mapWorkerStatus(
CoverageWorkerStatus _mapWorkerStatus(
dc.EnumValue<dc.ApplicationStatus> status,
) {
if (status is dc.Known<dc.ApplicationStatus>) {
switch (status.value) {
case dc.ApplicationStatus.LATE:
return 'late';
case dc.ApplicationStatus.CHECKED_IN:
case dc.ApplicationStatus.CHECKED_OUT:
case dc.ApplicationStatus.ACCEPTED:
case dc.ApplicationStatus.CONFIRMED:
case dc.ApplicationStatus.PENDING:
return CoverageWorkerStatus.pending;
case dc.ApplicationStatus.ACCEPTED:
return CoverageWorkerStatus.accepted;
case dc.ApplicationStatus.REJECTED:
return CoverageWorkerStatus.rejected;
case dc.ApplicationStatus.CONFIRMED:
return CoverageWorkerStatus.confirmed;
case dc.ApplicationStatus.CHECKED_IN:
return CoverageWorkerStatus.checkedIn;
case dc.ApplicationStatus.CHECKED_OUT:
return CoverageWorkerStatus.checkedOut;
case dc.ApplicationStatus.LATE:
return CoverageWorkerStatus.late;
case dc.ApplicationStatus.NO_SHOW:
return CoverageWorkerStatus.noShow;
case dc.ApplicationStatus.COMPLETED:
return 'confirmed';
return CoverageWorkerStatus.completed;
}
}
return 'confirmed';
return CoverageWorkerStatus.pending;
}
String? _formatTime(fdc.Timestamp? timestamp) {

View File

@@ -1,4 +1,4 @@
import '../ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// Repository interface for coverage-related operations.
///

View File

@@ -1,133 +0,0 @@
import 'package:equatable/equatable.dart';
/// Domain entity representing a shift in the coverage view.
///
/// This is a feature-specific domain entity that encapsulates shift information
/// including scheduling details and assigned workers.
class CoverageShift extends Equatable {
/// Creates a [CoverageShift].
const CoverageShift({
required this.id,
required this.title,
required this.location,
required this.startTime,
required this.workersNeeded,
required this.date,
required this.workers,
});
/// The unique identifier for the shift.
final String id;
/// The title or role of the shift.
final String title;
/// The location where the shift takes place.
final String location;
/// The start time of the shift (e.g., "16:00").
final String startTime;
/// The number of workers needed for this shift.
final int workersNeeded;
/// The date of the shift.
final DateTime date;
/// The list of workers assigned to this shift.
final List<CoverageWorker> workers;
/// Calculates the coverage percentage for this shift.
int get coveragePercent {
if (workersNeeded == 0) return 0;
return ((workers.length / workersNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
id,
title,
location,
startTime,
workersNeeded,
date,
workers,
];
}
/// Domain entity representing a worker in the coverage view.
///
/// This entity tracks worker status including check-in information.
class CoverageWorker extends Equatable {
/// Creates a [CoverageWorker].
const CoverageWorker({
required this.name,
required this.status,
this.checkInTime,
});
/// The name of the worker.
final String name;
/// The status of the worker ('confirmed', 'late', etc.).
final String status;
/// The time the worker checked in, if applicable.
final String? checkInTime;
/// Returns true if the worker is checked in.
bool get isCheckedIn => status == 'confirmed' && checkInTime != null;
/// Returns true if the worker is en route.
bool get isEnRoute => status == 'confirmed' && checkInTime == null;
/// Returns true if the worker is late.
bool get isLate => status == 'late';
@override
List<Object?> get props => <Object?>[name, status, checkInTime];
}
/// Domain entity representing coverage statistics.
///
/// Aggregates coverage metrics for a specific date.
class CoverageStats extends Equatable {
/// Creates a [CoverageStats].
const CoverageStats({
required this.totalNeeded,
required this.totalConfirmed,
required this.checkedIn,
required this.enRoute,
required this.late,
});
/// The total number of workers needed.
final int totalNeeded;
/// The total number of confirmed workers.
final int totalConfirmed;
/// The number of workers who have checked in.
final int checkedIn;
/// The number of workers en route.
final int enRoute;
/// The number of late workers.
final int late;
/// Calculates the overall coverage percentage.
int get coveragePercent {
if (totalNeeded == 0) return 0;
return ((totalConfirmed / totalNeeded) * 100).round();
}
@override
List<Object?> get props => <Object?>[
totalNeeded,
totalConfirmed,
checkedIn,
enRoute,
late,
];
}

View File

@@ -1,7 +1,8 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/get_coverage_stats_arguments.dart';
import '../repositories/coverage_repository.dart';
import '../ui_entities/coverage_entities.dart';
/// Use case for fetching coverage statistics for a specific date.
///

View File

@@ -1,7 +1,7 @@
import 'package:krow_core/core.dart';
import '../arguments/get_shifts_for_date_arguments.dart';
import '../repositories/coverage_repository.dart';
import '../ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// Use case for fetching shifts for a specific date.
///

View File

@@ -1,7 +1,7 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/arguments/get_coverage_stats_arguments.dart';
import '../../domain/arguments/get_shifts_for_date_arguments.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_coverage_stats_usecase.dart';
import '../../domain/usecases/get_shifts_for_date_usecase.dart';
import 'coverage_event.dart';

View File

@@ -1,5 +1,5 @@
import 'package:equatable/equatable.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// Enum representing the status of coverage data loading.
enum CoverageStatus {

View File

@@ -1,6 +1,6 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// Quick statistics cards showing coverage metrics.
///

View File

@@ -1,7 +1,7 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../domain/ui_entities/coverage_entities.dart';
import 'package:krow_domain/krow_domain.dart';
/// List of shifts with their workers.
///
@@ -194,7 +194,8 @@ class _ShiftHeader extends StatelessWidget {
size: UiConstants.space3,
color: UiColors.iconSecondary,
),
Expanded(child: Text(
Expanded(
child: Text(
location,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
@@ -314,36 +315,92 @@ class _WorkerRow extends StatelessWidget {
Color badgeText;
String badgeLabel;
if (worker.isCheckedIn) {
bg = UiColors.textSuccess.withOpacity(0.1);
border = UiColors.textSuccess;
textBg = UiColors.textSuccess.withOpacity(0.2);
textColor = UiColors.textSuccess;
icon = UiIcons.success;
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
badgeBg = UiColors.textSuccess;
badgeText = UiColors.primaryForeground;
badgeLabel = 'On Site';
} else if (worker.isEnRoute) {
bg = UiColors.textWarning.withOpacity(0.1);
border = UiColors.textWarning;
textBg = UiColors.textWarning.withOpacity(0.2);
textColor = UiColors.textWarning;
icon = UiIcons.clock;
statusText = 'En Route - Expected $shiftStartTime';
badgeBg = UiColors.textWarning;
badgeText = UiColors.primaryForeground;
badgeLabel = 'En Route';
} else {
bg = UiColors.destructive.withOpacity(0.1);
border = UiColors.destructive;
textBg = UiColors.destructive.withOpacity(0.2);
textColor = UiColors.destructive;
icon = UiIcons.warning;
statusText = '⚠ Running Late';
badgeBg = UiColors.destructive;
badgeText = UiColors.destructiveForeground;
badgeLabel = 'Late';
switch (worker.status) {
case CoverageWorkerStatus.checkedIn:
bg = UiColors.textSuccess.withOpacity(0.1);
border = UiColors.textSuccess;
textBg = UiColors.textSuccess.withOpacity(0.2);
textColor = UiColors.textSuccess;
icon = UiIcons.success;
statusText = '✓ Checked In at ${formatTime(worker.checkInTime)}';
badgeBg = UiColors.textSuccess;
badgeText = UiColors.primaryForeground;
badgeLabel = 'On Site';
case CoverageWorkerStatus.confirmed:
if (worker.checkInTime == null) {
bg = UiColors.textWarning.withOpacity(0.1);
border = UiColors.textWarning;
textBg = UiColors.textWarning.withOpacity(0.2);
textColor = UiColors.textWarning;
icon = UiIcons.clock;
statusText = 'En Route - Expected $shiftStartTime';
badgeBg = UiColors.textWarning;
badgeText = UiColors.primaryForeground;
badgeLabel = 'En Route';
} else {
bg = UiColors.muted.withOpacity(0.1);
border = UiColors.border;
textBg = UiColors.muted.withOpacity(0.2);
textColor = UiColors.textSecondary;
icon = UiIcons.success;
statusText = 'Confirmed';
badgeBg = UiColors.muted;
badgeText = UiColors.textPrimary;
badgeLabel = 'Confirmed';
}
case CoverageWorkerStatus.late:
bg = UiColors.destructive.withOpacity(0.1);
border = UiColors.destructive;
textBg = UiColors.destructive.withOpacity(0.2);
textColor = UiColors.destructive;
icon = UiIcons.warning;
statusText = '⚠ Running Late';
badgeBg = UiColors.destructive;
badgeText = UiColors.destructiveForeground;
badgeLabel = 'Late';
case CoverageWorkerStatus.checkedOut:
bg = UiColors.muted.withOpacity(0.1);
border = UiColors.border;
textBg = UiColors.muted.withOpacity(0.2);
textColor = UiColors.textSecondary;
icon = UiIcons.success;
statusText = 'Checked Out';
badgeBg = UiColors.muted;
badgeText = UiColors.textPrimary;
badgeLabel = 'Done';
case CoverageWorkerStatus.noShow:
bg = UiColors.destructive.withOpacity(0.1);
border = UiColors.destructive;
textBg = UiColors.destructive.withOpacity(0.2);
textColor = UiColors.destructive;
icon = UiIcons.warning;
statusText = 'No Show';
badgeBg = UiColors.destructive;
badgeText = UiColors.destructiveForeground;
badgeLabel = 'No Show';
case CoverageWorkerStatus.completed:
bg = UiColors.textSuccess.withOpacity(0.1);
border = UiColors.textSuccess;
textBg = UiColors.textSuccess.withOpacity(0.2);
textColor = UiColors.textSuccess;
icon = UiIcons.success;
statusText = 'Completed';
badgeBg = UiColors.textSuccess;
badgeText = UiColors.primaryForeground;
badgeLabel = 'Completed';
case CoverageWorkerStatus.pending:
case CoverageWorkerStatus.accepted:
case CoverageWorkerStatus.rejected:
bg = UiColors.muted.withOpacity(0.1);
border = UiColors.border;
textBg = UiColors.muted.withOpacity(0.2);
textColor = UiColors.textSecondary;
icon = UiIcons.clock;
statusText = worker.status.name.toUpperCase();
badgeBg = UiColors.muted;
badgeText = UiColors.textPrimary;
badgeLabel = worker.status.name[0].toUpperCase() +
worker.status.name.substring(1);
}
return Container(

View File

@@ -525,7 +525,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
@override
Future<void> acceptShift(String shiftId) async {
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED);
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.CONFIRMED);
}
@override