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:
Achintha Isuru
2026-03-19 13:23:28 -04:00
parent 5792aa6e98
commit 96056d0170
21 changed files with 1498 additions and 359 deletions

View File

@@ -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';

View File

@@ -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;
}

View File

@@ -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,
];
}

View File

@@ -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,
];
}

View File

@@ -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,
];
}

View File

@@ -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,
];
}