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

@@ -99,6 +99,7 @@ export 'src/entities/profile/accessibility.dart';
// Ratings
export 'src/entities/ratings/staff_rating.dart';
export 'src/entities/ratings/staff_reliability_stats.dart';
// Home
export 'src/entities/home/client_dashboard.dart';

View File

@@ -13,6 +13,7 @@ class AssignedWorker extends Equatable {
required this.fullName,
required this.status,
this.checkInAt,
this.hasReview = false,
});
/// Deserialises an [AssignedWorker] from a V2 API JSON map.
@@ -25,6 +26,7 @@ class AssignedWorker extends Equatable {
checkInAt: json['checkInAt'] != null
? DateTime.parse(json['checkInAt'] as String)
: null,
hasReview: json['hasReview'] as bool? ?? false,
);
}
@@ -43,6 +45,9 @@ class AssignedWorker extends Equatable {
/// When the worker clocked in (null if not yet).
final DateTime? checkInAt;
/// Whether this worker has already been reviewed for this assignment.
final bool hasReview;
/// Serialises this [AssignedWorker] to a JSON map.
Map<String, dynamic> toJson() {
return <String, dynamic>{
@@ -51,6 +56,7 @@ class AssignedWorker extends Equatable {
'fullName': fullName,
'status': status.toJson(),
'checkInAt': checkInAt?.toIso8601String(),
'hasReview': hasReview,
};
}
@@ -61,5 +67,6 @@ class AssignedWorker extends Equatable {
fullName,
status,
checkInAt,
hasReview,
];
}

View File

@@ -15,6 +15,8 @@ class ShiftWithWorkers extends Equatable {
required this.requiredWorkerCount,
required this.assignedWorkerCount,
this.assignedWorkers = const <AssignedWorker>[],
this.locationName = '',
this.locationAddress = '',
});
/// Deserialises a [ShiftWithWorkers] from a V2 API JSON map.
@@ -30,6 +32,8 @@ class ShiftWithWorkers extends Equatable {
return ShiftWithWorkers(
shiftId: json['shiftId'] as String,
roleName: json['roleName'] as String? ?? '',
locationName: json['locationName'] as String? ?? '',
locationAddress: json['locationAddress'] as String? ?? '',
timeRange: TimeRange.fromJson(json['timeRange'] as Map<String, dynamic>),
requiredWorkerCount: (json['requiredWorkerCount'] as num).toInt(),
assignedWorkerCount: (json['assignedWorkerCount'] as num).toInt(),
@@ -55,11 +59,19 @@ class ShiftWithWorkers extends Equatable {
/// List of assigned workers with their statuses.
final List<AssignedWorker> assignedWorkers;
/// Location or hub name for this shift.
final String locationName;
/// Street address for this shift.
final String locationAddress;
/// Serialises this [ShiftWithWorkers] to a JSON map.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'shiftId': shiftId,
'roleName': roleName,
'locationName': locationName,
'locationAddress': locationAddress,
'timeRange': timeRange.toJson(),
'requiredWorkerCount': requiredWorkerCount,
'assignedWorkerCount': assignedWorkerCount,
@@ -76,5 +88,7 @@ class ShiftWithWorkers extends Equatable {
requiredWorkerCount,
assignedWorkerCount,
assignedWorkers,
locationName,
locationAddress,
];
}

View File

@@ -0,0 +1,84 @@
import 'package:equatable/equatable.dart';
/// Aggregated reliability and performance statistics for a staff member.
///
/// Returned by `GET /staff/profile/stats`.
class StaffReliabilityStats extends Equatable {
/// Creates a [StaffReliabilityStats] instance.
const StaffReliabilityStats({
required this.staffId,
this.totalShifts = 0,
this.averageRating = 0,
this.ratingCount = 0,
this.onTimeRate = 0,
this.noShowCount = 0,
this.cancellationCount = 0,
this.reliabilityScore = 0,
});
/// Deserialises from a V2 API JSON map.
factory StaffReliabilityStats.fromJson(Map<String, dynamic> json) {
return StaffReliabilityStats(
staffId: json['staffId'] as String,
totalShifts: (json['totalShifts'] as num?)?.toInt() ?? 0,
averageRating: (json['averageRating'] as num?)?.toDouble() ?? 0,
ratingCount: (json['ratingCount'] as num?)?.toInt() ?? 0,
onTimeRate: (json['onTimeRate'] as num?)?.toDouble() ?? 0,
noShowCount: (json['noShowCount'] as num?)?.toInt() ?? 0,
cancellationCount: (json['cancellationCount'] as num?)?.toInt() ?? 0,
reliabilityScore: (json['reliabilityScore'] as num?)?.toDouble() ?? 0,
);
}
/// The staff member's unique identifier.
final String staffId;
/// Total completed shifts.
final int totalShifts;
/// Average rating from client reviews (0-5).
final double averageRating;
/// Number of ratings received.
final int ratingCount;
/// Percentage of shifts clocked in on time (0-100).
final double onTimeRate;
/// Number of no-show incidents.
final int noShowCount;
/// Number of worker-initiated cancellations.
final int cancellationCount;
/// Composite reliability score (0-100).
///
/// Weighted: 45% on-time rate + 35% completion rate + 20% rating score.
final double reliabilityScore;
/// Serialises to a JSON map.
Map<String, dynamic> toJson() {
return <String, dynamic>{
'staffId': staffId,
'totalShifts': totalShifts,
'averageRating': averageRating,
'ratingCount': ratingCount,
'onTimeRate': onTimeRate,
'noShowCount': noShowCount,
'cancellationCount': cancellationCount,
'reliabilityScore': reliabilityScore,
};
}
@override
List<Object?> get props => <Object?>[
staffId,
totalShifts,
averageRating,
ratingCount,
onTimeRate,
noShowCount,
cancellationCount,
reliabilityScore,
];
}