Add staff reliability stats & shift location

Introduce staff reliability statistics and location fields across domain, data, and UI. Changes include:

- New API endpoint StaffEndpoints.profileStats ('/staff/profile/stats').
- New domain entity StaffReliabilityStats with JSON (de)serialization and export.
- Profile repository: getReliabilityStats implementation and interface addition.
- New GetReliabilityStatsUseCase and DI registration in StaffProfileModule.
- ProfileCubit/state: load and store reliabilityStats; UI wired to display ReliabilityStatsCard and ReliabilityScoreBar using state values.
- Coverage/shift updates: Added AssignedWorker.hasReview to track if a worker was reviewed; added locationName/locationAddress to ShiftWithWorkers and show location in ShiftHeader; hide rate button if worker.hasReview.
- Clock-in handling: treat backend ALREADY_CLOCKED_IN (409) as idempotent by re-fetching attendance and emitting success when appropriate.

These changes wire backend stats through repository/usecase/cubit to the profile UI and add shift location and review-awareness to client views.
This commit is contained in:
Achintha Isuru
2026-03-19 10:53:37 -04:00
parent 88a319da4f
commit 493891eea0
15 changed files with 247 additions and 37 deletions

View File

@@ -161,6 +161,7 @@ class _CoverageShiftListState extends State<CoverageShiftList> {
children: <Widget>[
ShiftHeader(
title: shift.roleName,
locationName: shift.locationName,
startTime: _formatTime(shift.timeRange.startsAt),
current: shift.assignedWorkerCount,
total: shift.requiredWorkerCount,
@@ -226,9 +227,10 @@ class _CoverageShiftListState extends State<CoverageShiftList> {
worker: worker,
shiftStartTime: _formatTime(shift.timeRange.startsAt),
showRateButton:
worker.status == AssignmentStatus.checkedIn ||
worker.status == AssignmentStatus.checkedOut ||
worker.status == AssignmentStatus.completed,
!worker.hasReview &&
(worker.status == AssignmentStatus.checkedIn ||
worker.status == AssignmentStatus.checkedOut ||
worker.status == AssignmentStatus.completed),
showCancelButton:
DateTime.now().isAfter(shift.timeRange.startsAt) &&
(worker.status == AssignmentStatus.noShow ||

View File

@@ -21,6 +21,7 @@ class ShiftHeader extends StatelessWidget {
required this.lateCount,
required this.isExpanded,
required this.onToggle,
this.locationName,
super.key,
});
@@ -57,6 +58,9 @@ class ShiftHeader extends StatelessWidget {
/// Callback invoked when the header is tapped to expand or collapse.
final VoidCallback onToggle;
/// Optional location or hub name for the shift.
final String? locationName;
/// Returns the status colour based on [coveragePercent].
///
/// Green for >= 100 %, yellow for >= 80 %, red otherwise.
@@ -110,6 +114,29 @@ class ShiftHeader extends StatelessWidget {
title,
style: UiTypography.body1b.textPrimary,
),
if (locationName != null &&
locationName!.isNotEmpty) ...<Widget>[
const SizedBox(height: 2),
Row(
children: <Widget>[
const Icon(
UiIcons.mapPin,
size: 10,
color: UiColors.textSecondary,
),
const SizedBox(width: 4),
Expanded(
child: Text(
locationName!,
style: UiTypography.body3r.copyWith(
color: UiColors.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
const SizedBox(height: UiConstants.space1),
Row(
children: <Widget>[