feat: Implement available orders feature in staff marketplace
- Added `AvailableOrder` and `AvailableOrderSchedule` entities to represent available orders and their schedules. - Introduced `GetAvailableOrdersUseCase` and `BookOrderUseCase` for fetching and booking orders. - Created `AvailableOrdersBloc` to manage the state of available orders and handle booking actions. - Developed UI components including `AvailableOrderCard` to display order details and booking options. - Added necessary events and states for the BLoC architecture to support loading and booking orders. - Integrated new enums and utility functions for handling order types and scheduling.
This commit is contained in:
@@ -16,6 +16,7 @@ export 'src/entities/enums/benefit_status.dart';
|
||||
export 'src/entities/enums/business_status.dart';
|
||||
export 'src/entities/enums/invoice_status.dart';
|
||||
export 'src/entities/enums/onboarding_status.dart';
|
||||
export 'src/entities/enums/day_of_week.dart';
|
||||
export 'src/entities/enums/order_type.dart';
|
||||
export 'src/entities/enums/payment_status.dart';
|
||||
export 'src/entities/enums/review_issue_flag.dart';
|
||||
@@ -69,8 +70,12 @@ export 'src/entities/shifts/completed_shift.dart';
|
||||
export 'src/entities/shifts/shift_detail.dart';
|
||||
|
||||
// Orders
|
||||
export 'src/entities/orders/order_item.dart';
|
||||
export 'src/entities/orders/available_order.dart';
|
||||
export 'src/entities/orders/available_order_schedule.dart';
|
||||
export 'src/entities/orders/assigned_worker_summary.dart';
|
||||
export 'src/entities/orders/booking_assigned_shift.dart';
|
||||
export 'src/entities/orders/order_booking.dart';
|
||||
export 'src/entities/orders/order_item.dart';
|
||||
export 'src/entities/orders/order_preview.dart';
|
||||
export 'src/entities/orders/recent_order.dart';
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/// Day of the week for order scheduling.
|
||||
///
|
||||
/// Maps to the `day_of_week` values used in V2 order schedule responses.
|
||||
enum DayOfWeek {
|
||||
/// Monday.
|
||||
mon('MON'),
|
||||
|
||||
/// Tuesday.
|
||||
tue('TUE'),
|
||||
|
||||
/// Wednesday.
|
||||
wed('WED'),
|
||||
|
||||
/// Thursday.
|
||||
thu('THU'),
|
||||
|
||||
/// Friday.
|
||||
fri('FRI'),
|
||||
|
||||
/// Saturday.
|
||||
sat('SAT'),
|
||||
|
||||
/// Sunday.
|
||||
sun('SUN'),
|
||||
|
||||
/// Fallback for unrecognised API values.
|
||||
unknown('UNKNOWN');
|
||||
|
||||
const DayOfWeek(this.value);
|
||||
|
||||
/// The V2 API string representation.
|
||||
final String value;
|
||||
|
||||
/// Deserialises from a V2 API string with safe fallback.
|
||||
static DayOfWeek fromJson(String? value) {
|
||||
if (value == null) return DayOfWeek.unknown;
|
||||
final String upper = value.toUpperCase();
|
||||
for (final DayOfWeek day in DayOfWeek.values) {
|
||||
if (day.value == upper) return day;
|
||||
}
|
||||
return DayOfWeek.unknown;
|
||||
}
|
||||
|
||||
/// Serialises to the V2 API string.
|
||||
String toJson() => value;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/entities/enums/order_type.dart';
|
||||
import 'package:krow_domain/src/entities/orders/available_order_schedule.dart';
|
||||
|
||||
/// An available order in the staff marketplace.
|
||||
///
|
||||
/// Returned by `GET /staff/orders/available`. Represents an order-level card
|
||||
/// that a staff member can book into, containing role, location, pay rate,
|
||||
/// and schedule details.
|
||||
class AvailableOrder extends Equatable {
|
||||
/// Creates an [AvailableOrder].
|
||||
const AvailableOrder({
|
||||
required this.orderId,
|
||||
required this.orderType,
|
||||
required this.roleId,
|
||||
required this.roleCode,
|
||||
required this.roleName,
|
||||
this.clientName = '',
|
||||
this.location = '',
|
||||
this.locationAddress = '',
|
||||
required this.hourlyRateCents,
|
||||
required this.hourlyRate,
|
||||
required this.requiredWorkerCount,
|
||||
required this.filledCount,
|
||||
required this.instantBook,
|
||||
this.dispatchTeam = '',
|
||||
this.dispatchPriority = 0,
|
||||
required this.schedule,
|
||||
});
|
||||
|
||||
/// Deserialises from the V2 API JSON response.
|
||||
factory AvailableOrder.fromJson(Map<String, dynamic> json) {
|
||||
return AvailableOrder(
|
||||
orderId: json['orderId'] as String,
|
||||
orderType: OrderType.fromJson(json['orderType'] as String?),
|
||||
roleId: json['roleId'] as String,
|
||||
roleCode: json['roleCode'] as String? ?? '',
|
||||
roleName: json['roleName'] as String? ?? '',
|
||||
clientName: json['clientName'] as String? ?? '',
|
||||
location: json['location'] as String? ?? '',
|
||||
locationAddress: json['locationAddress'] as String? ?? '',
|
||||
hourlyRateCents: json['hourlyRateCents'] as int? ?? 0,
|
||||
hourlyRate: (json['hourlyRate'] as num?)?.toDouble() ?? 0.0,
|
||||
requiredWorkerCount: json['requiredWorkerCount'] as int? ?? 1,
|
||||
filledCount: json['filledCount'] as int? ?? 0,
|
||||
instantBook: json['instantBook'] as bool? ?? false,
|
||||
dispatchTeam: json['dispatchTeam'] as String? ?? '',
|
||||
dispatchPriority: json['dispatchPriority'] as int? ?? 0,
|
||||
schedule: AvailableOrderSchedule.fromJson(
|
||||
json['schedule'] as Map<String, dynamic>,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// The order row id.
|
||||
final String orderId;
|
||||
|
||||
/// Type of order (one-time, recurring, permanent, etc.).
|
||||
final OrderType orderType;
|
||||
|
||||
/// The shift-role row id.
|
||||
final String roleId;
|
||||
|
||||
/// Machine-readable role code.
|
||||
final String roleCode;
|
||||
|
||||
/// Display name of the role.
|
||||
final String roleName;
|
||||
|
||||
/// Name of the client/business offering this order.
|
||||
final String clientName;
|
||||
|
||||
/// Human-readable location label.
|
||||
final String location;
|
||||
|
||||
/// Full street address of the location.
|
||||
final String locationAddress;
|
||||
|
||||
/// Pay rate in cents per hour.
|
||||
final int hourlyRateCents;
|
||||
|
||||
/// Pay rate in dollars per hour.
|
||||
final double hourlyRate;
|
||||
|
||||
/// Total number of workers required for this role.
|
||||
final int requiredWorkerCount;
|
||||
|
||||
/// Number of positions already filled.
|
||||
final int filledCount;
|
||||
|
||||
/// Whether the order supports instant booking (no approval needed).
|
||||
final bool instantBook;
|
||||
|
||||
/// Dispatch team identifier.
|
||||
final String dispatchTeam;
|
||||
|
||||
/// Priority level for dispatch ordering.
|
||||
final int dispatchPriority;
|
||||
|
||||
/// Schedule details including recurrence, times, and bounding timestamps.
|
||||
final AvailableOrderSchedule schedule;
|
||||
|
||||
/// Serialises to JSON.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'orderId': orderId,
|
||||
'orderType': orderType.toJson(),
|
||||
'roleId': roleId,
|
||||
'roleCode': roleCode,
|
||||
'roleName': roleName,
|
||||
'clientName': clientName,
|
||||
'location': location,
|
||||
'locationAddress': locationAddress,
|
||||
'hourlyRateCents': hourlyRateCents,
|
||||
'hourlyRate': hourlyRate,
|
||||
'requiredWorkerCount': requiredWorkerCount,
|
||||
'filledCount': filledCount,
|
||||
'instantBook': instantBook,
|
||||
'dispatchTeam': dispatchTeam,
|
||||
'dispatchPriority': dispatchPriority,
|
||||
'schedule': schedule.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
orderId,
|
||||
orderType,
|
||||
roleId,
|
||||
roleCode,
|
||||
roleName,
|
||||
clientName,
|
||||
location,
|
||||
locationAddress,
|
||||
hourlyRateCents,
|
||||
hourlyRate,
|
||||
requiredWorkerCount,
|
||||
filledCount,
|
||||
instantBook,
|
||||
dispatchTeam,
|
||||
dispatchPriority,
|
||||
schedule,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/core/utils/utc_parser.dart';
|
||||
import 'package:krow_domain/src/entities/enums/day_of_week.dart';
|
||||
|
||||
/// Schedule details for an available order in the marketplace.
|
||||
///
|
||||
/// Contains the recurrence pattern, time window, and bounding timestamps
|
||||
/// for the order's shifts.
|
||||
class AvailableOrderSchedule extends Equatable {
|
||||
/// Creates an [AvailableOrderSchedule].
|
||||
const AvailableOrderSchedule({
|
||||
required this.totalShifts,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
required this.daysOfWeek,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.timezone,
|
||||
required this.firstShiftStartsAt,
|
||||
required this.lastShiftEndsAt,
|
||||
});
|
||||
|
||||
/// Deserialises from the V2 API JSON response.
|
||||
factory AvailableOrderSchedule.fromJson(Map<String, dynamic> json) {
|
||||
return AvailableOrderSchedule(
|
||||
totalShifts: json['totalShifts'] as int? ?? 0,
|
||||
startDate: json['startDate'] as String? ?? '',
|
||||
endDate: json['endDate'] as String? ?? '',
|
||||
daysOfWeek: (json['daysOfWeek'] as List<dynamic>?)
|
||||
?.map(
|
||||
(dynamic e) => DayOfWeek.fromJson(e as String),
|
||||
)
|
||||
.toList() ??
|
||||
<DayOfWeek>[],
|
||||
startTime: json['startTime'] as String? ?? '',
|
||||
endTime: json['endTime'] as String? ?? '',
|
||||
timezone: json['timezone'] as String? ?? 'UTC',
|
||||
firstShiftStartsAt:
|
||||
parseUtcToLocal(json['firstShiftStartsAt'] as String),
|
||||
lastShiftEndsAt: parseUtcToLocal(json['lastShiftEndsAt'] as String),
|
||||
);
|
||||
}
|
||||
|
||||
/// Total number of shifts in this schedule.
|
||||
final int totalShifts;
|
||||
|
||||
/// Date-only start string (e.g. "2026-03-24").
|
||||
final String startDate;
|
||||
|
||||
/// Date-only end string.
|
||||
final String endDate;
|
||||
|
||||
/// Days of the week the order repeats on.
|
||||
final List<DayOfWeek> daysOfWeek;
|
||||
|
||||
/// Daily start time display string (e.g. "09:00").
|
||||
final String startTime;
|
||||
|
||||
/// Daily end time display string (e.g. "15:00").
|
||||
final String endTime;
|
||||
|
||||
/// IANA timezone identifier (e.g. "America/Los_Angeles").
|
||||
final String timezone;
|
||||
|
||||
/// UTC timestamp of the first shift's start, converted to local time.
|
||||
final DateTime firstShiftStartsAt;
|
||||
|
||||
/// UTC timestamp of the last shift's end, converted to local time.
|
||||
final DateTime lastShiftEndsAt;
|
||||
|
||||
/// Serialises to JSON.
|
||||
Map<String, dynamic> toJson() => <String, dynamic>{
|
||||
'totalShifts': totalShifts,
|
||||
'startDate': startDate,
|
||||
'endDate': endDate,
|
||||
'daysOfWeek':
|
||||
daysOfWeek.map((DayOfWeek e) => e.toJson()).toList(),
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
'timezone': timezone,
|
||||
'firstShiftStartsAt':
|
||||
firstShiftStartsAt.toUtc().toIso8601String(),
|
||||
'lastShiftEndsAt': lastShiftEndsAt.toUtc().toIso8601String(),
|
||||
};
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
totalShifts,
|
||||
startDate,
|
||||
endDate,
|
||||
daysOfWeek,
|
||||
startTime,
|
||||
endTime,
|
||||
timezone,
|
||||
firstShiftStartsAt,
|
||||
lastShiftEndsAt,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/core/utils/utc_parser.dart';
|
||||
|
||||
/// A shift assigned to a staff member as part of an order booking.
|
||||
///
|
||||
/// Returned within the `assignedShifts` array of the
|
||||
/// `POST /staff/orders/:orderId/book` response.
|
||||
class BookingAssignedShift extends Equatable {
|
||||
/// Creates a [BookingAssignedShift].
|
||||
const BookingAssignedShift({
|
||||
required this.shiftId,
|
||||
required this.date,
|
||||
required this.startsAt,
|
||||
required this.endsAt,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.timezone,
|
||||
required this.assignmentId,
|
||||
this.assignmentStatus = '',
|
||||
});
|
||||
|
||||
/// Deserialises from the V2 API JSON response.
|
||||
factory BookingAssignedShift.fromJson(Map<String, dynamic> json) {
|
||||
return BookingAssignedShift(
|
||||
shiftId: json['shiftId'] as String,
|
||||
date: json['date'] as String? ?? '',
|
||||
startsAt: parseUtcToLocal(json['startsAt'] as String),
|
||||
endsAt: parseUtcToLocal(json['endsAt'] as String),
|
||||
startTime: json['startTime'] as String? ?? '',
|
||||
endTime: json['endTime'] as String? ?? '',
|
||||
timezone: json['timezone'] as String? ?? 'UTC',
|
||||
assignmentId: json['assignmentId'] as String,
|
||||
assignmentStatus: json['assignmentStatus'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// The shift row id.
|
||||
final String shiftId;
|
||||
|
||||
/// Date-only display string (e.g. "2026-03-24").
|
||||
final String date;
|
||||
|
||||
/// UTC start timestamp converted to local time.
|
||||
final DateTime startsAt;
|
||||
|
||||
/// UTC end timestamp converted to local time.
|
||||
final DateTime endsAt;
|
||||
|
||||
/// Display start time string (e.g. "09:00").
|
||||
final String startTime;
|
||||
|
||||
/// Display end time string (e.g. "15:00").
|
||||
final String endTime;
|
||||
|
||||
/// IANA timezone identifier.
|
||||
final String timezone;
|
||||
|
||||
/// The assignment row id linking staff to this shift.
|
||||
final String assignmentId;
|
||||
|
||||
/// Current status of the assignment (e.g. "ASSIGNED").
|
||||
final String assignmentStatus;
|
||||
|
||||
/// Serialises to JSON.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'shiftId': shiftId,
|
||||
'date': date,
|
||||
'startsAt': startsAt.toUtc().toIso8601String(),
|
||||
'endsAt': endsAt.toUtc().toIso8601String(),
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
'timezone': timezone,
|
||||
'assignmentId': assignmentId,
|
||||
'assignmentStatus': assignmentStatus,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
shiftId,
|
||||
date,
|
||||
startsAt,
|
||||
endsAt,
|
||||
startTime,
|
||||
endTime,
|
||||
timezone,
|
||||
assignmentId,
|
||||
assignmentStatus,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
import 'package:krow_domain/src/entities/orders/booking_assigned_shift.dart';
|
||||
|
||||
/// Result of booking an order via `POST /staff/orders/:orderId/book`.
|
||||
///
|
||||
/// Contains the booking metadata and the list of shifts assigned to the
|
||||
/// staff member as part of this booking.
|
||||
class OrderBooking extends Equatable {
|
||||
/// Creates an [OrderBooking].
|
||||
const OrderBooking({
|
||||
required this.bookingId,
|
||||
required this.orderId,
|
||||
required this.roleId,
|
||||
this.roleCode = '',
|
||||
this.roleName = '',
|
||||
required this.assignedShiftCount,
|
||||
this.status = 'PENDING',
|
||||
this.assignedShifts = const <BookingAssignedShift>[],
|
||||
});
|
||||
|
||||
/// Deserialises from the V2 API JSON response.
|
||||
factory OrderBooking.fromJson(Map<String, dynamic> json) {
|
||||
return OrderBooking(
|
||||
bookingId: json['bookingId'] as String,
|
||||
orderId: json['orderId'] as String,
|
||||
roleId: json['roleId'] as String,
|
||||
roleCode: json['roleCode'] as String? ?? '',
|
||||
roleName: json['roleName'] as String? ?? '',
|
||||
assignedShiftCount: json['assignedShiftCount'] as int? ?? 0,
|
||||
status: json['status'] as String? ?? 'PENDING',
|
||||
assignedShifts: (json['assignedShifts'] as List<dynamic>?)
|
||||
?.map(
|
||||
(dynamic e) => BookingAssignedShift.fromJson(
|
||||
e as Map<String, dynamic>,
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
<BookingAssignedShift>[],
|
||||
);
|
||||
}
|
||||
|
||||
/// Unique booking identifier.
|
||||
final String bookingId;
|
||||
|
||||
/// The order this booking belongs to.
|
||||
final String orderId;
|
||||
|
||||
/// The role row id within the order.
|
||||
final String roleId;
|
||||
|
||||
/// Machine-readable role code.
|
||||
final String roleCode;
|
||||
|
||||
/// Display name of the role.
|
||||
final String roleName;
|
||||
|
||||
/// Number of shifts assigned in this booking.
|
||||
final int assignedShiftCount;
|
||||
|
||||
/// Booking status (e.g. "PENDING", "CONFIRMED").
|
||||
final String status;
|
||||
|
||||
/// The individual shifts assigned as part of this booking.
|
||||
final List<BookingAssignedShift> assignedShifts;
|
||||
|
||||
/// Serialises to JSON.
|
||||
Map<String, dynamic> toJson() {
|
||||
return <String, dynamic>{
|
||||
'bookingId': bookingId,
|
||||
'orderId': orderId,
|
||||
'roleId': roleId,
|
||||
'roleCode': roleCode,
|
||||
'roleName': roleName,
|
||||
'assignedShiftCount': assignedShiftCount,
|
||||
'status': status,
|
||||
'assignedShifts': assignedShifts
|
||||
.map((BookingAssignedShift e) => e.toJson())
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => <Object?>[
|
||||
bookingId,
|
||||
orderId,
|
||||
roleId,
|
||||
roleCode,
|
||||
roleName,
|
||||
assignedShiftCount,
|
||||
status,
|
||||
assignedShifts,
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user