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:
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user