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:
@@ -14,6 +14,10 @@ abstract final class StaffEndpoints {
|
|||||||
static const ApiEndpoint profileCompletion =
|
static const ApiEndpoint profileCompletion =
|
||||||
ApiEndpoint('/staff/profile-completion');
|
ApiEndpoint('/staff/profile-completion');
|
||||||
|
|
||||||
|
/// Staff reliability and performance statistics.
|
||||||
|
static const ApiEndpoint profileStats =
|
||||||
|
ApiEndpoint('/staff/profile/stats');
|
||||||
|
|
||||||
/// Staff availability schedule.
|
/// Staff availability schedule.
|
||||||
static const ApiEndpoint availability = ApiEndpoint('/staff/availability');
|
static const ApiEndpoint availability = ApiEndpoint('/staff/availability');
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export 'src/entities/profile/accessibility.dart';
|
|||||||
|
|
||||||
// Ratings
|
// Ratings
|
||||||
export 'src/entities/ratings/staff_rating.dart';
|
export 'src/entities/ratings/staff_rating.dart';
|
||||||
|
export 'src/entities/ratings/staff_reliability_stats.dart';
|
||||||
|
|
||||||
// Home
|
// Home
|
||||||
export 'src/entities/home/client_dashboard.dart';
|
export 'src/entities/home/client_dashboard.dart';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class AssignedWorker extends Equatable {
|
|||||||
required this.fullName,
|
required this.fullName,
|
||||||
required this.status,
|
required this.status,
|
||||||
this.checkInAt,
|
this.checkInAt,
|
||||||
|
this.hasReview = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Deserialises an [AssignedWorker] from a V2 API JSON map.
|
/// Deserialises an [AssignedWorker] from a V2 API JSON map.
|
||||||
@@ -25,6 +26,7 @@ class AssignedWorker extends Equatable {
|
|||||||
checkInAt: json['checkInAt'] != null
|
checkInAt: json['checkInAt'] != null
|
||||||
? DateTime.parse(json['checkInAt'] as String)
|
? DateTime.parse(json['checkInAt'] as String)
|
||||||
: null,
|
: null,
|
||||||
|
hasReview: json['hasReview'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +45,9 @@ class AssignedWorker extends Equatable {
|
|||||||
/// When the worker clocked in (null if not yet).
|
/// When the worker clocked in (null if not yet).
|
||||||
final DateTime? checkInAt;
|
final DateTime? checkInAt;
|
||||||
|
|
||||||
|
/// Whether this worker has already been reviewed for this assignment.
|
||||||
|
final bool hasReview;
|
||||||
|
|
||||||
/// Serialises this [AssignedWorker] to a JSON map.
|
/// Serialises this [AssignedWorker] to a JSON map.
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
@@ -51,6 +56,7 @@ class AssignedWorker extends Equatable {
|
|||||||
'fullName': fullName,
|
'fullName': fullName,
|
||||||
'status': status.toJson(),
|
'status': status.toJson(),
|
||||||
'checkInAt': checkInAt?.toIso8601String(),
|
'checkInAt': checkInAt?.toIso8601String(),
|
||||||
|
'hasReview': hasReview,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,5 +67,6 @@ class AssignedWorker extends Equatable {
|
|||||||
fullName,
|
fullName,
|
||||||
status,
|
status,
|
||||||
checkInAt,
|
checkInAt,
|
||||||
|
hasReview,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ class ShiftWithWorkers extends Equatable {
|
|||||||
required this.requiredWorkerCount,
|
required this.requiredWorkerCount,
|
||||||
required this.assignedWorkerCount,
|
required this.assignedWorkerCount,
|
||||||
this.assignedWorkers = const <AssignedWorker>[],
|
this.assignedWorkers = const <AssignedWorker>[],
|
||||||
|
this.locationName = '',
|
||||||
|
this.locationAddress = '',
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Deserialises a [ShiftWithWorkers] from a V2 API JSON map.
|
/// Deserialises a [ShiftWithWorkers] from a V2 API JSON map.
|
||||||
@@ -30,6 +32,8 @@ class ShiftWithWorkers extends Equatable {
|
|||||||
return ShiftWithWorkers(
|
return ShiftWithWorkers(
|
||||||
shiftId: json['shiftId'] as String,
|
shiftId: json['shiftId'] as String,
|
||||||
roleName: json['roleName'] 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>),
|
timeRange: TimeRange.fromJson(json['timeRange'] as Map<String, dynamic>),
|
||||||
requiredWorkerCount: (json['requiredWorkerCount'] as num).toInt(),
|
requiredWorkerCount: (json['requiredWorkerCount'] as num).toInt(),
|
||||||
assignedWorkerCount: (json['assignedWorkerCount'] as num).toInt(),
|
assignedWorkerCount: (json['assignedWorkerCount'] as num).toInt(),
|
||||||
@@ -55,11 +59,19 @@ class ShiftWithWorkers extends Equatable {
|
|||||||
/// List of assigned workers with their statuses.
|
/// List of assigned workers with their statuses.
|
||||||
final List<AssignedWorker> assignedWorkers;
|
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.
|
/// Serialises this [ShiftWithWorkers] to a JSON map.
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return <String, dynamic>{
|
return <String, dynamic>{
|
||||||
'shiftId': shiftId,
|
'shiftId': shiftId,
|
||||||
'roleName': roleName,
|
'roleName': roleName,
|
||||||
|
'locationName': locationName,
|
||||||
|
'locationAddress': locationAddress,
|
||||||
'timeRange': timeRange.toJson(),
|
'timeRange': timeRange.toJson(),
|
||||||
'requiredWorkerCount': requiredWorkerCount,
|
'requiredWorkerCount': requiredWorkerCount,
|
||||||
'assignedWorkerCount': assignedWorkerCount,
|
'assignedWorkerCount': assignedWorkerCount,
|
||||||
@@ -76,5 +88,7 @@ class ShiftWithWorkers extends Equatable {
|
|||||||
requiredWorkerCount,
|
requiredWorkerCount,
|
||||||
assignedWorkerCount,
|
assignedWorkerCount,
|
||||||
assignedWorkers,
|
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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -161,6 +161,7 @@ class _CoverageShiftListState extends State<CoverageShiftList> {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
ShiftHeader(
|
ShiftHeader(
|
||||||
title: shift.roleName,
|
title: shift.roleName,
|
||||||
|
locationName: shift.locationName,
|
||||||
startTime: _formatTime(shift.timeRange.startsAt),
|
startTime: _formatTime(shift.timeRange.startsAt),
|
||||||
current: shift.assignedWorkerCount,
|
current: shift.assignedWorkerCount,
|
||||||
total: shift.requiredWorkerCount,
|
total: shift.requiredWorkerCount,
|
||||||
@@ -226,9 +227,10 @@ class _CoverageShiftListState extends State<CoverageShiftList> {
|
|||||||
worker: worker,
|
worker: worker,
|
||||||
shiftStartTime: _formatTime(shift.timeRange.startsAt),
|
shiftStartTime: _formatTime(shift.timeRange.startsAt),
|
||||||
showRateButton:
|
showRateButton:
|
||||||
worker.status == AssignmentStatus.checkedIn ||
|
!worker.hasReview &&
|
||||||
worker.status == AssignmentStatus.checkedOut ||
|
(worker.status == AssignmentStatus.checkedIn ||
|
||||||
worker.status == AssignmentStatus.completed,
|
worker.status == AssignmentStatus.checkedOut ||
|
||||||
|
worker.status == AssignmentStatus.completed),
|
||||||
showCancelButton:
|
showCancelButton:
|
||||||
DateTime.now().isAfter(shift.timeRange.startsAt) &&
|
DateTime.now().isAfter(shift.timeRange.startsAt) &&
|
||||||
(worker.status == AssignmentStatus.noShow ||
|
(worker.status == AssignmentStatus.noShow ||
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class ShiftHeader extends StatelessWidget {
|
|||||||
required this.lateCount,
|
required this.lateCount,
|
||||||
required this.isExpanded,
|
required this.isExpanded,
|
||||||
required this.onToggle,
|
required this.onToggle,
|
||||||
|
this.locationName,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -57,6 +58,9 @@ class ShiftHeader extends StatelessWidget {
|
|||||||
/// Callback invoked when the header is tapped to expand or collapse.
|
/// Callback invoked when the header is tapped to expand or collapse.
|
||||||
final VoidCallback onToggle;
|
final VoidCallback onToggle;
|
||||||
|
|
||||||
|
/// Optional location or hub name for the shift.
|
||||||
|
final String? locationName;
|
||||||
|
|
||||||
/// Returns the status colour based on [coveragePercent].
|
/// Returns the status colour based on [coveragePercent].
|
||||||
///
|
///
|
||||||
/// Green for >= 100 %, yellow for >= 80 %, red otherwise.
|
/// Green for >= 100 %, yellow for >= 80 %, red otherwise.
|
||||||
@@ -110,6 +114,29 @@ class ShiftHeader extends StatelessWidget {
|
|||||||
title,
|
title,
|
||||||
style: UiTypography.body1b.textPrimary,
|
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),
|
const SizedBox(height: UiConstants.space1),
|
||||||
Row(
|
Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
|||||||
@@ -228,12 +228,20 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
|
|||||||
event: event,
|
event: event,
|
||||||
activeShiftId: newStatus.activeShiftId,
|
activeShiftId: newStatus.activeShiftId,
|
||||||
);
|
);
|
||||||
} on AppException catch (_) {
|
} on AppException catch (e) {
|
||||||
// The clock-in API call failed. Re-fetch attendance status to
|
// The backend returns 409 ALREADY_CLOCKED_IN when the worker has
|
||||||
// reconcile: if the worker is already clocked in (e.g. duplicate
|
// an active attendance session. This is a normal idempotency
|
||||||
// session from Postgres constraint 23505), treat it as success.
|
// signal — re-fetch the authoritative status and emit success
|
||||||
|
// without surfacing an error snackbar.
|
||||||
|
final bool isAlreadyClockedIn =
|
||||||
|
e is ApiException && e.apiCode == 'ALREADY_CLOCKED_IN';
|
||||||
|
|
||||||
|
// Re-fetch attendance status to reconcile local state with
|
||||||
|
// the backend (handles both ALREADY_CLOCKED_IN and legacy
|
||||||
|
// Postgres constraint 23505 duplicates).
|
||||||
final AttendanceStatus currentStatus = await _getAttendanceStatus();
|
final AttendanceStatus currentStatus = await _getAttendanceStatus();
|
||||||
if (currentStatus.isClockedIn) {
|
|
||||||
|
if (isAlreadyClockedIn || currentStatus.isClockedIn) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: ClockInStatus.success,
|
status: ClockInStatus.success,
|
||||||
attendance: currentStatus,
|
attendance: currentStatus,
|
||||||
|
|||||||
@@ -31,6 +31,15 @@ class ProfileRepositoryImpl implements ProfileRepositoryInterface {
|
|||||||
return ProfileSectionStatus.fromJson(json);
|
return ProfileSectionStatus.fromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StaffReliabilityStats> getReliabilityStats() async {
|
||||||
|
final ApiResponse response =
|
||||||
|
await _api.get(StaffEndpoints.profileStats);
|
||||||
|
final Map<String, dynamic> json =
|
||||||
|
response.data as Map<String, dynamic>;
|
||||||
|
return StaffReliabilityStats.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
await _api.post(AuthEndpoints.signOut);
|
await _api.post(AuthEndpoints.signOut);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
/// Abstract interface for the staff profile repository.
|
/// Abstract interface for the staff profile repository.
|
||||||
///
|
///
|
||||||
/// Defines the contract for fetching staff profile data,
|
/// Defines the contract for fetching staff profile data,
|
||||||
/// section completion statuses, and signing out.
|
/// section completion statuses, reliability stats, and signing out.
|
||||||
abstract interface class ProfileRepositoryInterface {
|
abstract interface class ProfileRepositoryInterface {
|
||||||
/// Fetches the staff profile from the backend.
|
/// Fetches the staff profile from the backend.
|
||||||
Future<Staff> getStaffProfile();
|
Future<Staff> getStaffProfile();
|
||||||
@@ -11,6 +11,9 @@ abstract interface class ProfileRepositoryInterface {
|
|||||||
/// Fetches the profile section completion statuses.
|
/// Fetches the profile section completion statuses.
|
||||||
Future<ProfileSectionStatus> getProfileSections();
|
Future<ProfileSectionStatus> getProfileSections();
|
||||||
|
|
||||||
|
/// Fetches reliability and performance statistics for the staff member.
|
||||||
|
Future<StaffReliabilityStats> getReliabilityStats();
|
||||||
|
|
||||||
/// Signs out the current user.
|
/// Signs out the current user.
|
||||||
Future<void> signOut();
|
Future<void> signOut();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'package:staff_profile/src/domain/repositories/profile_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Use case for retrieving the staff member's reliability statistics.
|
||||||
|
class GetReliabilityStatsUseCase
|
||||||
|
implements NoInputUseCase<StaffReliabilityStats> {
|
||||||
|
/// Creates a [GetReliabilityStatsUseCase] with the required [repository].
|
||||||
|
GetReliabilityStatsUseCase(this._repository);
|
||||||
|
|
||||||
|
final ProfileRepositoryInterface _repository;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StaffReliabilityStats> call() {
|
||||||
|
return _repository.getReliabilityStats();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import 'package:krow_core/core.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import 'package:staff_profile/src/domain/usecases/get_profile_sections_usecase.dart';
|
import 'package:staff_profile/src/domain/usecases/get_profile_sections_usecase.dart';
|
||||||
|
import 'package:staff_profile/src/domain/usecases/get_reliability_stats_usecase.dart';
|
||||||
import 'package:staff_profile/src/domain/usecases/get_staff_profile_usecase.dart';
|
import 'package:staff_profile/src/domain/usecases/get_staff_profile_usecase.dart';
|
||||||
import 'package:staff_profile/src/domain/usecases/sign_out_usecase.dart';
|
import 'package:staff_profile/src/domain/usecases/sign_out_usecase.dart';
|
||||||
import 'package:staff_profile/src/presentation/blocs/profile_state.dart';
|
import 'package:staff_profile/src/presentation/blocs/profile_state.dart';
|
||||||
@@ -10,21 +11,24 @@ import 'package:staff_profile/src/presentation/blocs/profile_state.dart';
|
|||||||
/// Cubit for managing the Profile feature state.
|
/// Cubit for managing the Profile feature state.
|
||||||
///
|
///
|
||||||
/// Delegates all data fetching to use cases, following Clean Architecture.
|
/// Delegates all data fetching to use cases, following Clean Architecture.
|
||||||
/// Loads the staff profile and section completion statuses in a single flow.
|
/// Loads the staff profile, section statuses, and reliability stats.
|
||||||
class ProfileCubit extends Cubit<ProfileState>
|
class ProfileCubit extends Cubit<ProfileState>
|
||||||
with BlocErrorHandler<ProfileState> {
|
with BlocErrorHandler<ProfileState> {
|
||||||
/// Creates a [ProfileCubit] with the required use cases.
|
/// Creates a [ProfileCubit] with the required use cases.
|
||||||
ProfileCubit({
|
ProfileCubit({
|
||||||
required GetStaffProfileUseCase getStaffProfileUseCase,
|
required GetStaffProfileUseCase getStaffProfileUseCase,
|
||||||
required GetProfileSectionsUseCase getProfileSectionsUseCase,
|
required GetProfileSectionsUseCase getProfileSectionsUseCase,
|
||||||
|
required GetReliabilityStatsUseCase getReliabilityStatsUseCase,
|
||||||
required SignOutUseCase signOutUseCase,
|
required SignOutUseCase signOutUseCase,
|
||||||
}) : _getStaffProfileUseCase = getStaffProfileUseCase,
|
}) : _getStaffProfileUseCase = getStaffProfileUseCase,
|
||||||
_getProfileSectionsUseCase = getProfileSectionsUseCase,
|
_getProfileSectionsUseCase = getProfileSectionsUseCase,
|
||||||
|
_getReliabilityStatsUseCase = getReliabilityStatsUseCase,
|
||||||
_signOutUseCase = signOutUseCase,
|
_signOutUseCase = signOutUseCase,
|
||||||
super(const ProfileState());
|
super(const ProfileState());
|
||||||
|
|
||||||
final GetStaffProfileUseCase _getStaffProfileUseCase;
|
final GetStaffProfileUseCase _getStaffProfileUseCase;
|
||||||
final GetProfileSectionsUseCase _getProfileSectionsUseCase;
|
final GetProfileSectionsUseCase _getProfileSectionsUseCase;
|
||||||
|
final GetReliabilityStatsUseCase _getReliabilityStatsUseCase;
|
||||||
final SignOutUseCase _signOutUseCase;
|
final SignOutUseCase _signOutUseCase;
|
||||||
|
|
||||||
/// Loads the staff member's profile.
|
/// Loads the staff member's profile.
|
||||||
@@ -62,6 +66,19 @@ class ProfileCubit extends Cubit<ProfileState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Loads reliability and performance statistics for the staff member.
|
||||||
|
Future<void> loadReliabilityStats() async {
|
||||||
|
await handleError(
|
||||||
|
emit: emit,
|
||||||
|
action: () async {
|
||||||
|
final StaffReliabilityStats stats =
|
||||||
|
await _getReliabilityStatsUseCase();
|
||||||
|
emit(state.copyWith(reliabilityStats: stats));
|
||||||
|
},
|
||||||
|
onError: (String _) => state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Signs out the current user.
|
/// Signs out the current user.
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
if (state.status == ProfileStatus.loading) {
|
if (state.status == ProfileStatus.loading) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class ProfileState extends Equatable {
|
|||||||
const ProfileState({
|
const ProfileState({
|
||||||
this.status = ProfileStatus.initial,
|
this.status = ProfileStatus.initial,
|
||||||
this.profile,
|
this.profile,
|
||||||
|
this.reliabilityStats,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.personalInfoComplete,
|
this.personalInfoComplete,
|
||||||
this.emergencyContactsComplete,
|
this.emergencyContactsComplete,
|
||||||
@@ -37,40 +38,45 @@ class ProfileState extends Equatable {
|
|||||||
this.documentsComplete,
|
this.documentsComplete,
|
||||||
this.certificatesComplete,
|
this.certificatesComplete,
|
||||||
});
|
});
|
||||||
/// Current status of the profile feature
|
|
||||||
|
/// Current status of the profile feature.
|
||||||
final ProfileStatus status;
|
final ProfileStatus status;
|
||||||
|
|
||||||
/// The staff member's profile object (null if not loaded)
|
/// The staff member's profile object (null if not loaded).
|
||||||
final Staff? profile;
|
final Staff? profile;
|
||||||
|
|
||||||
/// Error message if status is error
|
/// Reliability and performance statistics (null if not loaded).
|
||||||
|
final StaffReliabilityStats? reliabilityStats;
|
||||||
|
|
||||||
|
/// Error message if status is error.
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
/// Whether personal information is complete
|
/// Whether personal information is complete.
|
||||||
final bool? personalInfoComplete;
|
final bool? personalInfoComplete;
|
||||||
|
|
||||||
/// Whether emergency contacts are complete
|
/// Whether emergency contacts are complete.
|
||||||
final bool? emergencyContactsComplete;
|
final bool? emergencyContactsComplete;
|
||||||
|
|
||||||
/// Whether experience information is complete
|
/// Whether experience information is complete.
|
||||||
final bool? experienceComplete;
|
final bool? experienceComplete;
|
||||||
|
|
||||||
/// Whether tax forms are complete
|
/// Whether tax forms are complete.
|
||||||
final bool? taxFormsComplete;
|
final bool? taxFormsComplete;
|
||||||
|
|
||||||
/// Whether attire options are complete
|
/// Whether attire options are complete.
|
||||||
final bool? attireComplete;
|
final bool? attireComplete;
|
||||||
|
|
||||||
/// Whether documents are complete
|
/// Whether documents are complete.
|
||||||
final bool? documentsComplete;
|
final bool? documentsComplete;
|
||||||
|
|
||||||
/// Whether certificates are complete
|
/// Whether certificates are complete.
|
||||||
final bool? certificatesComplete;
|
final bool? certificatesComplete;
|
||||||
|
|
||||||
/// Creates a copy of this state with updated values.
|
/// Creates a copy of this state with updated values.
|
||||||
ProfileState copyWith({
|
ProfileState copyWith({
|
||||||
ProfileStatus? status,
|
ProfileStatus? status,
|
||||||
Staff? profile,
|
Staff? profile,
|
||||||
|
StaffReliabilityStats? reliabilityStats,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
bool? personalInfoComplete,
|
bool? personalInfoComplete,
|
||||||
bool? emergencyContactsComplete,
|
bool? emergencyContactsComplete,
|
||||||
@@ -83,6 +89,7 @@ class ProfileState extends Equatable {
|
|||||||
return ProfileState(
|
return ProfileState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
profile: profile ?? this.profile,
|
profile: profile ?? this.profile,
|
||||||
|
reliabilityStats: reliabilityStats ?? this.reliabilityStats,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
personalInfoComplete: personalInfoComplete ?? this.personalInfoComplete,
|
personalInfoComplete: personalInfoComplete ?? this.personalInfoComplete,
|
||||||
emergencyContactsComplete: emergencyContactsComplete ?? this.emergencyContactsComplete,
|
emergencyContactsComplete: emergencyContactsComplete ?? this.emergencyContactsComplete,
|
||||||
@@ -98,6 +105,7 @@ class ProfileState extends Equatable {
|
|||||||
List<Object?> get props => <Object?>[
|
List<Object?> get props => <Object?>[
|
||||||
status,
|
status,
|
||||||
profile,
|
profile,
|
||||||
|
reliabilityStats,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
personalInfoComplete,
|
personalInfoComplete,
|
||||||
emergencyContactsComplete,
|
emergencyContactsComplete,
|
||||||
|
|||||||
@@ -37,10 +37,11 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
value: cubit,
|
value: cubit,
|
||||||
child: BlocConsumer<ProfileCubit, ProfileState>(
|
child: BlocConsumer<ProfileCubit, ProfileState>(
|
||||||
listener: (BuildContext context, ProfileState state) {
|
listener: (BuildContext context, ProfileState state) {
|
||||||
// Load section statuses when profile loads successfully
|
// Load section statuses and reliability stats when profile loads
|
||||||
if (state.status == ProfileStatus.loaded &&
|
if (state.status == ProfileStatus.loaded &&
|
||||||
state.personalInfoComplete == null) {
|
state.personalInfoComplete == null) {
|
||||||
cubit.loadSectionStatuses();
|
cubit.loadSectionStatuses();
|
||||||
|
cubit.loadReliabilityStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status == ProfileStatus.signedOut) {
|
if (state.status == ProfileStatus.signedOut) {
|
||||||
@@ -100,16 +101,16 @@ class StaffProfilePage extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
// Reliability Stats
|
// Reliability Stats
|
||||||
ReliabilityStatsCard(
|
ReliabilityStatsCard(
|
||||||
totalShifts: 0,
|
totalShifts: state.reliabilityStats?.totalShifts,
|
||||||
averageRating: profile.averageRating,
|
averageRating: state.reliabilityStats?.averageRating,
|
||||||
onTimeRate: 0,
|
onTimeRate: state.reliabilityStats?.onTimeRate.round(),
|
||||||
noShowCount: 0,
|
noShowCount: state.reliabilityStats?.noShowCount,
|
||||||
cancellationCount: 0,
|
cancellationCount: state.reliabilityStats?.cancellationCount,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Reliability Score Bar
|
// Reliability Score Bar
|
||||||
const ReliabilityScoreBar(
|
ReliabilityScoreBar(
|
||||||
reliabilityScore: 0,
|
reliabilityScore: state.reliabilityStats?.reliabilityScore.round(),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Ordered sections
|
// Ordered sections
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import 'package:staff_profile/src/data/repositories/profile_repository_impl.dart';
|
import 'package:staff_profile/src/data/repositories/profile_repository_impl.dart';
|
||||||
import 'package:staff_profile/src/domain/repositories/profile_repository_interface.dart';
|
import 'package:staff_profile/src/domain/repositories/profile_repository_interface.dart';
|
||||||
import 'package:staff_profile/src/domain/usecases/get_profile_sections_usecase.dart';
|
import 'package:staff_profile/src/domain/usecases/get_profile_sections_usecase.dart';
|
||||||
|
import 'package:staff_profile/src/domain/usecases/get_reliability_stats_usecase.dart';
|
||||||
import 'package:staff_profile/src/domain/usecases/get_staff_profile_usecase.dart';
|
import 'package:staff_profile/src/domain/usecases/get_staff_profile_usecase.dart';
|
||||||
import 'package:staff_profile/src/domain/usecases/sign_out_usecase.dart';
|
import 'package:staff_profile/src/domain/usecases/sign_out_usecase.dart';
|
||||||
import 'package:staff_profile/src/presentation/blocs/profile_cubit.dart';
|
import 'package:staff_profile/src/presentation/blocs/profile_cubit.dart';
|
||||||
@@ -44,12 +45,18 @@ class StaffProfileModule extends Module {
|
|||||||
i.get<ProfileRepositoryInterface>(),
|
i.get<ProfileRepositoryInterface>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
i.addLazySingleton<GetReliabilityStatsUseCase>(
|
||||||
|
() => GetReliabilityStatsUseCase(
|
||||||
|
i.get<ProfileRepositoryInterface>(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Cubit
|
// Cubit
|
||||||
i.addLazySingleton<ProfileCubit>(
|
i.addLazySingleton<ProfileCubit>(
|
||||||
() => ProfileCubit(
|
() => ProfileCubit(
|
||||||
getStaffProfileUseCase: i.get<GetStaffProfileUseCase>(),
|
getStaffProfileUseCase: i.get<GetStaffProfileUseCase>(),
|
||||||
getProfileSectionsUseCase: i.get<GetProfileSectionsUseCase>(),
|
getProfileSectionsUseCase: i.get<GetProfileSectionsUseCase>(),
|
||||||
|
getReliabilityStatsUseCase: i.get<GetReliabilityStatsUseCase>(),
|
||||||
signOutUseCase: i.get<SignOutUseCase>(),
|
signOutUseCase: i.get<SignOutUseCase>(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user