feat(shifts): implement submit for approval functionality

- Added `submitForApproval` method to `ShiftsRepositoryInterface` and its implementation in `ShiftsRepositoryImpl`.
- Created `SubmitForApprovalUseCase` to handle the submission logic.
- Updated `ShiftsBloc` to handle `SubmitForApprovalEvent` and manage submission state.
- Enhanced `HistoryShiftsTab` and `MyShiftsTab` to support submission actions and display appropriate UI feedback.
- Refactored date utilities for better calendar management and filtering of past shifts.
- Improved UI components for better spacing and alignment.
- Localized success messages for shift submission actions.
This commit is contained in:
Achintha Isuru
2026-03-18 14:37:55 -04:00
parent 3e5b6af8dc
commit 3a5f2cc9c6
50 changed files with 1269 additions and 408 deletions

View File

@@ -18,6 +18,10 @@ class AssignedShift extends Equatable {
required this.startTime,
required this.endTime,
required this.hourlyRateCents,
required this.hourlyRate,
required this.totalRateCents,
required this.totalRate,
required this.clientName,
required this.orderType,
required this.status,
});
@@ -33,6 +37,10 @@ class AssignedShift extends Equatable {
startTime: DateTime.parse(json['startTime'] as String),
endTime: DateTime.parse(json['endTime'] as String),
hourlyRateCents: json['hourlyRateCents'] as int? ?? 0,
hourlyRate: (json['hourlyRate'] as num?)?.toDouble() ?? 0.0,
totalRateCents: json['totalRateCents'] as int? ?? 0,
totalRate: (json['totalRate'] as num?)?.toDouble() ?? 0.0,
clientName: json['clientName'] as String? ?? '',
orderType: OrderType.fromJson(json['orderType'] as String?),
status: AssignmentStatus.fromJson(json['status'] as String?),
);
@@ -62,6 +70,18 @@ class AssignedShift extends Equatable {
/// Pay rate in cents per hour.
final int hourlyRateCents;
/// Pay rate in dollars per hour.
final double hourlyRate;
/// Total pay for this shift in cents.
final int totalRateCents;
/// Total pay for this shift in dollars.
final double totalRate;
/// Name of the client / business for this shift.
final String clientName;
/// Order type.
final OrderType orderType;
@@ -79,6 +99,10 @@ class AssignedShift extends Equatable {
'startTime': startTime.toIso8601String(),
'endTime': endTime.toIso8601String(),
'hourlyRateCents': hourlyRateCents,
'hourlyRate': hourlyRate,
'totalRateCents': totalRateCents,
'totalRate': totalRate,
'clientName': clientName,
'orderType': orderType.toJson(),
'status': status.toJson(),
};
@@ -94,6 +118,10 @@ class AssignedShift extends Equatable {
startTime,
endTime,
hourlyRateCents,
hourlyRate,
totalRateCents,
totalRate,
clientName,
orderType,
status,
];

View File

@@ -12,10 +12,18 @@ class CompletedShift extends Equatable {
required this.shiftId,
required this.title,
required this.location,
required this.clientName,
required this.date,
required this.startTime,
required this.endTime,
required this.minutesWorked,
required this.hourlyRateCents,
required this.hourlyRate,
required this.totalRateCents,
required this.totalRate,
required this.paymentStatus,
required this.status,
this.timesheetStatus,
});
/// Deserialises from the V2 API JSON response.
@@ -25,10 +33,22 @@ class CompletedShift extends Equatable {
shiftId: json['shiftId'] as String,
title: json['title'] as String? ?? '',
location: json['location'] as String? ?? '',
clientName: json['clientName'] as String? ?? '',
date: DateTime.parse(json['date'] as String),
startTime: json['startTime'] != null
? DateTime.parse(json['startTime'] as String)
: DateTime.now(),
endTime: json['endTime'] != null
? DateTime.parse(json['endTime'] as String)
: DateTime.now(),
minutesWorked: json['minutesWorked'] as int? ?? 0,
hourlyRateCents: json['hourlyRateCents'] as int? ?? 0,
hourlyRate: (json['hourlyRate'] as num?)?.toDouble() ?? 0.0,
totalRateCents: json['totalRateCents'] as int? ?? 0,
totalRate: (json['totalRate'] as num?)?.toDouble() ?? 0.0,
paymentStatus: PaymentStatus.fromJson(json['paymentStatus'] as String?),
status: AssignmentStatus.completed,
timesheetStatus: json['timesheetStatus'] as String?,
);
}
@@ -44,18 +64,42 @@ class CompletedShift extends Equatable {
/// Human-readable location label.
final String location;
/// Name of the client / business for this shift.
final String clientName;
/// The date the shift was worked.
final DateTime date;
/// Scheduled start time.
final DateTime startTime;
/// Scheduled end time.
final DateTime endTime;
/// Total minutes worked (regular + overtime).
final int minutesWorked;
/// Pay rate in cents per hour.
final int hourlyRateCents;
/// Pay rate in dollars per hour.
final double hourlyRate;
/// Total pay for this shift in cents.
final int totalRateCents;
/// Total pay for this shift in dollars.
final double totalRate;
/// Payment processing status.
final PaymentStatus paymentStatus;
/// Assignment status (should always be `completed` for this class).
final AssignmentStatus status;
/// Timesheet status (e.g. `SUBMITTED`, `APPROVED`, `PAID`, or null).
final String? timesheetStatus;
/// Serialises to JSON.
Map<String, dynamic> toJson() {
return <String, dynamic>{
@@ -63,9 +107,17 @@ class CompletedShift extends Equatable {
'shiftId': shiftId,
'title': title,
'location': location,
'clientName': clientName,
'date': date.toIso8601String(),
'startTime': startTime.toIso8601String(),
'endTime': endTime.toIso8601String(),
'minutesWorked': minutesWorked,
'hourlyRateCents': hourlyRateCents,
'hourlyRate': hourlyRate,
'totalRateCents': totalRateCents,
'totalRate': totalRate,
'paymentStatus': paymentStatus.toJson(),
'timesheetStatus': timesheetStatus,
};
}
@@ -75,8 +127,17 @@ class CompletedShift extends Equatable {
shiftId,
title,
location,
clientName,
date,
startTime,
endTime,
minutesWorked,
hourlyRateCents,
hourlyRate,
totalRateCents,
totalRate,
paymentStatus,
timesheetStatus,
status,
];
}

View File

@@ -17,6 +17,7 @@ class OpenShift extends Equatable {
required this.startTime,
required this.endTime,
required this.hourlyRateCents,
required this.hourlyRate,
required this.orderType,
required this.instantBook,
required this.requiredWorkerCount,
@@ -33,6 +34,7 @@ class OpenShift extends Equatable {
startTime: DateTime.parse(json['startTime'] as String),
endTime: DateTime.parse(json['endTime'] as String),
hourlyRateCents: json['hourlyRateCents'] as int? ?? 0,
hourlyRate: (json['hourlyRate'] as num?)?.toDouble() ?? 0.0,
orderType: OrderType.fromJson(json['orderType'] as String?),
instantBook: json['instantBook'] as bool? ?? false,
requiredWorkerCount: json['requiredWorkerCount'] as int? ?? 1,
@@ -63,6 +65,9 @@ class OpenShift extends Equatable {
/// Pay rate in cents per hour.
final int hourlyRateCents;
/// Pay rate in dollars per hour.
final double hourlyRate;
/// Order type.
final OrderType orderType;
@@ -83,6 +88,7 @@ class OpenShift extends Equatable {
'startTime': startTime.toIso8601String(),
'endTime': endTime.toIso8601String(),
'hourlyRateCents': hourlyRateCents,
'hourlyRate': hourlyRate,
'orderType': orderType.toJson(),
'instantBook': instantBook,
'requiredWorkerCount': requiredWorkerCount,
@@ -99,6 +105,7 @@ class OpenShift extends Equatable {
startTime,
endTime,
hourlyRateCents,
hourlyRate,
orderType,
instantBook,
requiredWorkerCount,

View File

@@ -11,7 +11,7 @@ class Shift extends Equatable {
/// Creates a [Shift].
const Shift({
required this.id,
required this.orderId,
this.orderId,
required this.title,
required this.status,
required this.startsAt,
@@ -25,13 +25,16 @@ class Shift extends Equatable {
required this.requiredWorkers,
required this.assignedWorkers,
this.notes,
this.clockInMode,
this.allowClockInOverride,
this.nfcTagId,
});
/// Deserialises from the V2 API JSON response.
factory Shift.fromJson(Map<String, dynamic> json) {
return Shift(
id: json['id'] as String,
orderId: json['orderId'] as String,
orderId: json['orderId'] as String?,
title: json['title'] as String? ?? '',
status: ShiftStatus.fromJson(json['status'] as String?),
startsAt: DateTime.parse(json['startsAt'] as String),
@@ -45,14 +48,17 @@ class Shift extends Equatable {
requiredWorkers: json['requiredWorkers'] as int? ?? 1,
assignedWorkers: json['assignedWorkers'] as int? ?? 0,
notes: json['notes'] as String?,
clockInMode: json['clockInMode'] as String?,
allowClockInOverride: json['allowClockInOverride'] as bool?,
nfcTagId: json['nfcTagId'] as String?,
);
}
/// The shift row id.
final String id;
/// The parent order id.
final String orderId;
/// The parent order id (may be null for today-shifts endpoint).
final String? orderId;
/// Display title.
final String title;
@@ -93,6 +99,15 @@ class Shift extends Equatable {
/// Free-form notes for the shift.
final String? notes;
/// Clock-in mode for this shift (`NFC_REQUIRED`, `GEO_REQUIRED`, `EITHER`).
final String? clockInMode;
/// Whether the worker is allowed to override the clock-in method.
final bool? allowClockInOverride;
/// NFC tag identifier for NFC-based clock-in.
final String? nfcTagId;
/// Serialises to JSON.
Map<String, dynamic> toJson() {
return <String, dynamic>{
@@ -111,6 +126,9 @@ class Shift extends Equatable {
'requiredWorkers': requiredWorkers,
'assignedWorkers': assignedWorkers,
'notes': notes,
'clockInMode': clockInMode,
'allowClockInOverride': allowClockInOverride,
'nfcTagId': nfcTagId,
};
}
@@ -140,5 +158,8 @@ class Shift extends Equatable {
requiredWorkers,
assignedWorkers,
notes,
clockInMode,
allowClockInOverride,
nfcTagId,
];
}

View File

@@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart';
import 'package:krow_domain/src/entities/enums/application_status.dart';
import 'package:krow_domain/src/entities/enums/assignment_status.dart';
import 'package:krow_domain/src/entities/enums/order_type.dart';
import 'package:krow_domain/src/entities/shifts/shift.dart';
/// Full detail view of a shift for the staff member.
///
@@ -18,17 +19,27 @@ class ShiftDetail extends Equatable {
this.description,
required this.location,
this.address,
required this.clientName,
this.latitude,
this.longitude,
required this.date,
required this.startTime,
required this.endTime,
required this.roleId,
required this.roleName,
required this.hourlyRateCents,
required this.hourlyRate,
required this.totalRateCents,
required this.totalRate,
required this.orderType,
required this.requiredCount,
required this.confirmedCount,
this.assignmentStatus,
this.applicationStatus,
this.clockInMode,
required this.allowClockInOverride,
this.geofenceRadiusMeters,
this.nfcTagId,
});
/// Deserialises from the V2 API JSON response.
@@ -39,12 +50,18 @@ class ShiftDetail extends Equatable {
description: json['description'] as String?,
location: json['location'] as String? ?? '',
address: json['address'] as String?,
clientName: json['clientName'] as String? ?? '',
latitude: Shift.parseDouble(json['latitude']),
longitude: Shift.parseDouble(json['longitude']),
date: DateTime.parse(json['date'] as String),
startTime: DateTime.parse(json['startTime'] as String),
endTime: DateTime.parse(json['endTime'] as String),
roleId: json['roleId'] as String,
roleName: json['roleName'] as String,
hourlyRateCents: json['hourlyRateCents'] as int? ?? 0,
hourlyRate: (json['hourlyRate'] as num?)?.toDouble() ?? 0.0,
totalRateCents: json['totalRateCents'] as int? ?? 0,
totalRate: (json['totalRate'] as num?)?.toDouble() ?? 0.0,
orderType: OrderType.fromJson(json['orderType'] as String?),
requiredCount: json['requiredCount'] as int? ?? 1,
confirmedCount: json['confirmedCount'] as int? ?? 0,
@@ -54,6 +71,10 @@ class ShiftDetail extends Equatable {
applicationStatus: json['applicationStatus'] != null
? ApplicationStatus.fromJson(json['applicationStatus'] as String?)
: null,
clockInMode: json['clockInMode'] as String?,
allowClockInOverride: json['allowClockInOverride'] as bool? ?? false,
geofenceRadiusMeters: json['geofenceRadiusMeters'] as int?,
nfcTagId: json['nfcTagId'] as String?,
);
}
@@ -72,6 +93,15 @@ class ShiftDetail extends Equatable {
/// Street address of the shift location.
final String? address;
/// Name of the client / business for this shift.
final String clientName;
/// Latitude for map display and geofence validation.
final double? latitude;
/// Longitude for map display and geofence validation.
final double? longitude;
/// Date of the shift (same as startTime, kept for display grouping).
final DateTime date;
@@ -90,6 +120,15 @@ class ShiftDetail extends Equatable {
/// Pay rate in cents per hour.
final int hourlyRateCents;
/// Pay rate in dollars per hour.
final double hourlyRate;
/// Total pay for this shift in cents.
final int totalRateCents;
/// Total pay for this shift in dollars.
final double totalRate;
/// Order type.
final OrderType orderType;
@@ -105,6 +144,26 @@ class ShiftDetail extends Equatable {
/// Current worker's application status, if applied.
final ApplicationStatus? applicationStatus;
/// Clock-in mode for this shift (`NFC_REQUIRED`, `GEO_REQUIRED`, `EITHER`).
final String? clockInMode;
/// Whether the worker is allowed to override the clock-in method.
final bool allowClockInOverride;
/// Geofence radius in meters for clock-in validation.
final int? geofenceRadiusMeters;
/// NFC tag identifier for NFC-based clock-in.
final String? nfcTagId;
/// Duration of the shift in hours.
double get durationHours {
return endTime.difference(startTime).inMinutes / 60;
}
/// Estimated total pay in dollars.
double get estimatedTotal => hourlyRate * durationHours;
/// Serialises to JSON.
Map<String, dynamic> toJson() {
return <String, dynamic>{
@@ -113,17 +172,27 @@ class ShiftDetail extends Equatable {
'description': description,
'location': location,
'address': address,
'clientName': clientName,
'latitude': latitude,
'longitude': longitude,
'date': date.toIso8601String(),
'startTime': startTime.toIso8601String(),
'endTime': endTime.toIso8601String(),
'roleId': roleId,
'roleName': roleName,
'hourlyRateCents': hourlyRateCents,
'hourlyRate': hourlyRate,
'totalRateCents': totalRateCents,
'totalRate': totalRate,
'orderType': orderType.toJson(),
'requiredCount': requiredCount,
'confirmedCount': confirmedCount,
'assignmentStatus': assignmentStatus?.toJson(),
'applicationStatus': applicationStatus?.toJson(),
'clockInMode': clockInMode,
'allowClockInOverride': allowClockInOverride,
'geofenceRadiusMeters': geofenceRadiusMeters,
'nfcTagId': nfcTagId,
};
}
@@ -134,16 +203,26 @@ class ShiftDetail extends Equatable {
description,
location,
address,
clientName,
latitude,
longitude,
date,
startTime,
endTime,
roleId,
roleName,
hourlyRateCents,
hourlyRate,
totalRateCents,
totalRate,
orderType,
requiredCount,
confirmedCount,
assignmentStatus,
applicationStatus,
clockInMode,
allowClockInOverride,
geofenceRadiusMeters,
nfcTagId,
];
}