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:
@@ -92,7 +92,7 @@ abstract final class ClientEndpoints {
|
|||||||
|
|
||||||
/// View orders.
|
/// View orders.
|
||||||
static const ApiEndpoint ordersView =
|
static const ApiEndpoint ordersView =
|
||||||
ApiEndpoint('/client/orders/view');
|
ApiEndpoint('/client/shifts/scheduled');
|
||||||
|
|
||||||
/// Order reorder preview.
|
/// Order reorder preview.
|
||||||
static ApiEndpoint orderReorderPreview(String orderId) =>
|
static ApiEndpoint orderReorderPreview(String orderId) =>
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ abstract final class StaffEndpoints {
|
|||||||
/// FAQs search.
|
/// FAQs search.
|
||||||
static const ApiEndpoint faqsSearch = ApiEndpoint('/staff/faqs/search');
|
static const ApiEndpoint faqsSearch = ApiEndpoint('/staff/faqs/search');
|
||||||
|
|
||||||
|
/// Available orders for the marketplace.
|
||||||
|
static const ApiEndpoint ordersAvailable =
|
||||||
|
ApiEndpoint('/staff/orders/available');
|
||||||
|
|
||||||
// ── Write ─────────────────────────────────────────────────────────────
|
// ── Write ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Staff profile setup.
|
/// Staff profile setup.
|
||||||
@@ -198,6 +202,10 @@ abstract final class StaffEndpoints {
|
|||||||
static const ApiEndpoint locationStreams =
|
static const ApiEndpoint locationStreams =
|
||||||
ApiEndpoint('/staff/location-streams');
|
ApiEndpoint('/staff/location-streams');
|
||||||
|
|
||||||
|
/// Book an available order.
|
||||||
|
static ApiEndpoint orderBook(String orderId) =>
|
||||||
|
ApiEndpoint('/staff/orders/$orderId/book');
|
||||||
|
|
||||||
/// Register or delete device push token (POST to register, DELETE to remove).
|
/// Register or delete device push token (POST to register, DELETE to remove).
|
||||||
static const ApiEndpoint devicesPushTokens =
|
static const ApiEndpoint devicesPushTokens =
|
||||||
ApiEndpoint('/staff/devices/push-tokens');
|
ApiEndpoint('/staff/devices/push-tokens');
|
||||||
|
|||||||
@@ -1877,5 +1877,18 @@
|
|||||||
"success_message": "Cash out request submitted!",
|
"success_message": "Cash out request submitted!",
|
||||||
"fee_notice": "A small fee of \\$1.99 may apply for instant transfers."
|
"fee_notice": "A small fee of \\$1.99 may apply for instant transfers."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"available_orders": {
|
||||||
|
"book_order": "Book Order",
|
||||||
|
"apply": "Apply",
|
||||||
|
"spots_left": "${count} spot(s) left",
|
||||||
|
"shifts_count": "${count} shift(s)",
|
||||||
|
"booking_success": "Order booked successfully!",
|
||||||
|
"booking_pending": "Your booking is pending approval",
|
||||||
|
"booking_confirmed": "Your booking has been confirmed!",
|
||||||
|
"no_orders": "No orders available",
|
||||||
|
"no_orders_subtitle": "Check back later for new opportunities",
|
||||||
|
"instant_book": "Instant Book",
|
||||||
|
"per_hour": "/hr"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1877,5 +1877,18 @@
|
|||||||
"success_message": "\u00a1Solicitud de retiro enviada!",
|
"success_message": "\u00a1Solicitud de retiro enviada!",
|
||||||
"fee_notice": "Puede aplicarse una peque\u00f1a tarifa de \\$1.99 para transferencias instant\u00e1neas."
|
"fee_notice": "Puede aplicarse una peque\u00f1a tarifa de \\$1.99 para transferencias instant\u00e1neas."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"available_orders": {
|
||||||
|
"book_order": "Reservar Orden",
|
||||||
|
"apply": "Aplicar",
|
||||||
|
"spots_left": "${count} puesto(s) disponible(s)",
|
||||||
|
"shifts_count": "${count} turno(s)",
|
||||||
|
"booking_success": "\u00a1Orden reservada con \u00e9xito!",
|
||||||
|
"booking_pending": "Tu reserva est\u00e1 pendiente de aprobaci\u00f3n",
|
||||||
|
"booking_confirmed": "\u00a1Tu reserva ha sido confirmada!",
|
||||||
|
"no_orders": "No hay \u00f3rdenes disponibles",
|
||||||
|
"no_orders_subtitle": "Vuelve m\u00e1s tarde para nuevas oportunidades",
|
||||||
|
"instant_book": "Reserva Instant\u00e1nea",
|
||||||
|
"per_hour": "/hr"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ export 'src/entities/enums/benefit_status.dart';
|
|||||||
export 'src/entities/enums/business_status.dart';
|
export 'src/entities/enums/business_status.dart';
|
||||||
export 'src/entities/enums/invoice_status.dart';
|
export 'src/entities/enums/invoice_status.dart';
|
||||||
export 'src/entities/enums/onboarding_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/order_type.dart';
|
||||||
export 'src/entities/enums/payment_status.dart';
|
export 'src/entities/enums/payment_status.dart';
|
||||||
export 'src/entities/enums/review_issue_flag.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';
|
export 'src/entities/shifts/shift_detail.dart';
|
||||||
|
|
||||||
// Orders
|
// 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/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/order_preview.dart';
|
||||||
export 'src/entities/orders/recent_order.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,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -165,4 +165,38 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
final ProfileCompletion completion = ProfileCompletion.fromJson(data);
|
final ProfileCompletion completion = ProfileCompletion.fromJson(data);
|
||||||
return completion.completed;
|
return completion.completed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<AvailableOrder>> getAvailableOrders({
|
||||||
|
String? search,
|
||||||
|
int limit = 20,
|
||||||
|
}) async {
|
||||||
|
final Map<String, dynamic> params = <String, dynamic>{
|
||||||
|
'limit': limit,
|
||||||
|
};
|
||||||
|
if (search != null && search.isNotEmpty) {
|
||||||
|
params['search'] = search;
|
||||||
|
}
|
||||||
|
final ApiResponse response = await _apiService.get(
|
||||||
|
StaffEndpoints.ordersAvailable,
|
||||||
|
params: params,
|
||||||
|
);
|
||||||
|
final List<dynamic> items = _extractItems(response.data);
|
||||||
|
return items
|
||||||
|
.map((dynamic json) =>
|
||||||
|
AvailableOrder.fromJson(json as Map<String, dynamic>))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<OrderBooking> bookOrder({
|
||||||
|
required String orderId,
|
||||||
|
required String roleId,
|
||||||
|
}) async {
|
||||||
|
final ApiResponse response = await _apiService.post(
|
||||||
|
StaffEndpoints.orderBook(orderId),
|
||||||
|
data: <String, dynamic>{'roleId': roleId},
|
||||||
|
);
|
||||||
|
return OrderBooking.fromJson(response.data as Map<String, dynamic>);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,4 +52,16 @@ abstract interface class ShiftsRepositoryInterface {
|
|||||||
///
|
///
|
||||||
/// Only allowed for shifts in CHECKED_OUT or COMPLETED status.
|
/// Only allowed for shifts in CHECKED_OUT or COMPLETED status.
|
||||||
Future<void> submitForApproval(String shiftId, {String? note});
|
Future<void> submitForApproval(String shiftId, {String? note});
|
||||||
|
|
||||||
|
/// Retrieves available orders from the staff marketplace.
|
||||||
|
Future<List<AvailableOrder>> getAvailableOrders({
|
||||||
|
String? search,
|
||||||
|
int limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Books an order by placing the staff member into a role.
|
||||||
|
Future<OrderBooking> bookOrder({
|
||||||
|
required String orderId,
|
||||||
|
required String roleId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Books an available order for the current staff member.
|
||||||
|
///
|
||||||
|
/// Delegates to [ShiftsRepositoryInterface.bookOrder] with the order and
|
||||||
|
/// role identifiers.
|
||||||
|
class BookOrderUseCase {
|
||||||
|
/// Creates a [BookOrderUseCase].
|
||||||
|
BookOrderUseCase(this._repository);
|
||||||
|
|
||||||
|
/// The shifts repository.
|
||||||
|
final ShiftsRepositoryInterface _repository;
|
||||||
|
|
||||||
|
/// Executes the use case, returning the [OrderBooking] result.
|
||||||
|
Future<OrderBooking> call({
|
||||||
|
required String orderId,
|
||||||
|
required String roleId,
|
||||||
|
}) {
|
||||||
|
return _repository.bookOrder(orderId: orderId, roleId: roleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
|
/// Retrieves available orders from the staff marketplace.
|
||||||
|
///
|
||||||
|
/// Delegates to [ShiftsRepositoryInterface.getAvailableOrders] with an
|
||||||
|
/// optional search filter.
|
||||||
|
class GetAvailableOrdersUseCase {
|
||||||
|
/// Creates a [GetAvailableOrdersUseCase].
|
||||||
|
GetAvailableOrdersUseCase(this._repository);
|
||||||
|
|
||||||
|
/// The shifts repository.
|
||||||
|
final ShiftsRepositoryInterface _repository;
|
||||||
|
|
||||||
|
/// Executes the use case, returning a list of [AvailableOrder].
|
||||||
|
Future<List<AvailableOrder>> call({String? search, int limit = 20}) {
|
||||||
|
return _repository.getAvailableOrders(search: search, limit: limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'package:staff_shifts/src/domain/usecases/book_order_usecase.dart';
|
||||||
|
import 'package:staff_shifts/src/domain/usecases/get_available_orders_usecase.dart';
|
||||||
|
|
||||||
|
import 'available_orders_event.dart';
|
||||||
|
import 'available_orders_state.dart';
|
||||||
|
|
||||||
|
/// Manages the state for the available-orders marketplace tab.
|
||||||
|
///
|
||||||
|
/// Loads order-level cards from `GET /staff/orders/available` and handles
|
||||||
|
/// booking via `POST /staff/orders/:orderId/book`.
|
||||||
|
class AvailableOrdersBloc
|
||||||
|
extends Bloc<AvailableOrdersEvent, AvailableOrdersState>
|
||||||
|
with BlocErrorHandler<AvailableOrdersState> {
|
||||||
|
/// Creates an [AvailableOrdersBloc].
|
||||||
|
AvailableOrdersBloc({
|
||||||
|
required GetAvailableOrdersUseCase getAvailableOrders,
|
||||||
|
required BookOrderUseCase bookOrder,
|
||||||
|
}) : _getAvailableOrders = getAvailableOrders,
|
||||||
|
_bookOrder = bookOrder,
|
||||||
|
super(const AvailableOrdersState()) {
|
||||||
|
on<LoadAvailableOrdersEvent>(_onLoadAvailableOrders);
|
||||||
|
on<BookOrderEvent>(_onBookOrder);
|
||||||
|
on<ClearBookingResultEvent>(_onClearBookingResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use case for fetching available orders.
|
||||||
|
final GetAvailableOrdersUseCase _getAvailableOrders;
|
||||||
|
|
||||||
|
/// Use case for booking an order.
|
||||||
|
final BookOrderUseCase _bookOrder;
|
||||||
|
|
||||||
|
Future<void> _onLoadAvailableOrders(
|
||||||
|
LoadAvailableOrdersEvent event,
|
||||||
|
Emitter<AvailableOrdersState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: AvailableOrdersStatus.loading,
|
||||||
|
clearErrorMessage: true,
|
||||||
|
));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
final List<AvailableOrder> orders =
|
||||||
|
await _getAvailableOrders(search: event.search);
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: AvailableOrdersStatus.loaded,
|
||||||
|
orders: orders,
|
||||||
|
clearErrorMessage: true,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
status: AvailableOrdersStatus.error,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onBookOrder(
|
||||||
|
BookOrderEvent event,
|
||||||
|
Emitter<AvailableOrdersState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(bookingInProgress: true, clearErrorMessage: true));
|
||||||
|
|
||||||
|
await handleError(
|
||||||
|
emit: emit.call,
|
||||||
|
action: () async {
|
||||||
|
final OrderBooking booking = await _bookOrder(
|
||||||
|
orderId: event.orderId,
|
||||||
|
roleId: event.roleId,
|
||||||
|
);
|
||||||
|
emit(state.copyWith(
|
||||||
|
bookingInProgress: false,
|
||||||
|
lastBooking: booking,
|
||||||
|
clearErrorMessage: true,
|
||||||
|
));
|
||||||
|
// Reload orders after successful booking.
|
||||||
|
add(const LoadAvailableOrdersEvent());
|
||||||
|
},
|
||||||
|
onError: (String errorKey) => state.copyWith(
|
||||||
|
bookingInProgress: false,
|
||||||
|
errorMessage: errorKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onClearBookingResult(
|
||||||
|
ClearBookingResultEvent event,
|
||||||
|
Emitter<AvailableOrdersState> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(clearLastBooking: true, clearErrorMessage: true));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
/// Base class for all available-orders events.
|
||||||
|
@immutable
|
||||||
|
sealed class AvailableOrdersEvent extends Equatable {
|
||||||
|
/// Creates an [AvailableOrdersEvent].
|
||||||
|
const AvailableOrdersEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads available orders from the staff marketplace.
|
||||||
|
class LoadAvailableOrdersEvent extends AvailableOrdersEvent {
|
||||||
|
/// Creates a [LoadAvailableOrdersEvent].
|
||||||
|
const LoadAvailableOrdersEvent({this.search});
|
||||||
|
|
||||||
|
/// Optional search query to filter orders.
|
||||||
|
final String? search;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[search];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Books the staff member into an order for a specific role.
|
||||||
|
class BookOrderEvent extends AvailableOrdersEvent {
|
||||||
|
/// Creates a [BookOrderEvent].
|
||||||
|
const BookOrderEvent({required this.orderId, required this.roleId});
|
||||||
|
|
||||||
|
/// The order to book.
|
||||||
|
final String orderId;
|
||||||
|
|
||||||
|
/// The role within the order to fill.
|
||||||
|
final String roleId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[orderId, roleId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the last booking result so the UI can dismiss confirmation.
|
||||||
|
class ClearBookingResultEvent extends AvailableOrdersEvent {
|
||||||
|
/// Creates a [ClearBookingResultEvent].
|
||||||
|
const ClearBookingResultEvent();
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// Lifecycle status for the available-orders list.
|
||||||
|
enum AvailableOrdersStatus {
|
||||||
|
/// No data has been requested yet.
|
||||||
|
initial,
|
||||||
|
|
||||||
|
/// A load is in progress.
|
||||||
|
loading,
|
||||||
|
|
||||||
|
/// Data has been loaded successfully.
|
||||||
|
loaded,
|
||||||
|
|
||||||
|
/// An error occurred during loading.
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// State for the available-orders marketplace tab.
|
||||||
|
class AvailableOrdersState extends Equatable {
|
||||||
|
/// Creates an [AvailableOrdersState].
|
||||||
|
const AvailableOrdersState({
|
||||||
|
this.status = AvailableOrdersStatus.initial,
|
||||||
|
this.orders = const <AvailableOrder>[],
|
||||||
|
this.bookingInProgress = false,
|
||||||
|
this.lastBooking,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Current lifecycle status.
|
||||||
|
final AvailableOrdersStatus status;
|
||||||
|
|
||||||
|
/// The list of available orders.
|
||||||
|
final List<AvailableOrder> orders;
|
||||||
|
|
||||||
|
/// Whether a booking request is currently in flight.
|
||||||
|
final bool bookingInProgress;
|
||||||
|
|
||||||
|
/// The result of the most recent booking, if any.
|
||||||
|
final OrderBooking? lastBooking;
|
||||||
|
|
||||||
|
/// Error message key for display.
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
/// Creates a copy with the given fields replaced.
|
||||||
|
AvailableOrdersState copyWith({
|
||||||
|
AvailableOrdersStatus? status,
|
||||||
|
List<AvailableOrder>? orders,
|
||||||
|
bool? bookingInProgress,
|
||||||
|
OrderBooking? lastBooking,
|
||||||
|
bool clearLastBooking = false,
|
||||||
|
String? errorMessage,
|
||||||
|
bool clearErrorMessage = false,
|
||||||
|
}) {
|
||||||
|
return AvailableOrdersState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
orders: orders ?? this.orders,
|
||||||
|
bookingInProgress: bookingInProgress ?? this.bookingInProgress,
|
||||||
|
lastBooking:
|
||||||
|
clearLastBooking ? null : (lastBooking ?? this.lastBooking),
|
||||||
|
errorMessage:
|
||||||
|
clearErrorMessage ? null : (errorMessage ?? this.errorMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => <Object?>[
|
||||||
|
status,
|
||||||
|
orders,
|
||||||
|
bookingInProgress,
|
||||||
|
lastBooking,
|
||||||
|
errorMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -5,6 +5,9 @@ import 'package:design_system/design_system.dart';
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import 'package:staff_shifts/src/presentation/blocs/available_orders/available_orders_bloc.dart';
|
||||||
|
import 'package:staff_shifts/src/presentation/blocs/available_orders/available_orders_event.dart';
|
||||||
|
import 'package:staff_shifts/src/presentation/blocs/available_orders/available_orders_state.dart';
|
||||||
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
|
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
|
||||||
import 'package:staff_shifts/src/presentation/utils/shift_tab_type.dart';
|
import 'package:staff_shifts/src/presentation/utils/shift_tab_type.dart';
|
||||||
import 'package:staff_shifts/src/presentation/widgets/shifts_page_skeleton.dart';
|
import 'package:staff_shifts/src/presentation/widgets/shifts_page_skeleton.dart';
|
||||||
@@ -14,7 +17,8 @@ import 'package:staff_shifts/src/presentation/widgets/tabs/history_shifts_tab.da
|
|||||||
|
|
||||||
/// Tabbed page for browsing staff shifts (My Shifts, Find Work, History).
|
/// Tabbed page for browsing staff shifts (My Shifts, Find Work, History).
|
||||||
///
|
///
|
||||||
/// Manages tab state locally and delegates data loading to [ShiftsBloc].
|
/// Manages tab state locally and delegates data loading to [ShiftsBloc]
|
||||||
|
/// and [AvailableOrdersBloc].
|
||||||
class ShiftsPage extends StatefulWidget {
|
class ShiftsPage extends StatefulWidget {
|
||||||
/// Creates a [ShiftsPage].
|
/// Creates a [ShiftsPage].
|
||||||
///
|
///
|
||||||
@@ -45,9 +49,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
late ShiftTabType _activeTab;
|
late ShiftTabType _activeTab;
|
||||||
DateTime? _selectedDate;
|
DateTime? _selectedDate;
|
||||||
bool _prioritizeFind = false;
|
bool _prioritizeFind = false;
|
||||||
bool _refreshAvailable = false;
|
|
||||||
bool _pendingAvailableRefresh = false;
|
bool _pendingAvailableRefresh = false;
|
||||||
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
|
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
|
||||||
|
final AvailableOrdersBloc _ordersBloc = Modular.get<AvailableOrdersBloc>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -55,7 +59,6 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
_activeTab = widget.initialTab ?? ShiftTabType.find;
|
_activeTab = widget.initialTab ?? ShiftTabType.find;
|
||||||
_selectedDate = widget.selectedDate;
|
_selectedDate = widget.selectedDate;
|
||||||
_prioritizeFind = _activeTab == ShiftTabType.find;
|
_prioritizeFind = _activeTab == ShiftTabType.find;
|
||||||
_refreshAvailable = widget.refreshAvailable;
|
|
||||||
_pendingAvailableRefresh = widget.refreshAvailable;
|
_pendingAvailableRefresh = widget.refreshAvailable;
|
||||||
if (_prioritizeFind) {
|
if (_prioritizeFind) {
|
||||||
_bloc.add(LoadFindFirstEvent());
|
_bloc.add(LoadFindFirstEvent());
|
||||||
@@ -66,9 +69,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
_bloc.add(LoadHistoryShiftsEvent());
|
_bloc.add(LoadHistoryShiftsEvent());
|
||||||
}
|
}
|
||||||
if (_activeTab == ShiftTabType.find) {
|
if (_activeTab == ShiftTabType.find) {
|
||||||
if (!_prioritizeFind) {
|
// Load available orders via the new BLoC.
|
||||||
_bloc.add(LoadAvailableShiftsEvent(force: _refreshAvailable));
|
_ordersBloc.add(const LoadAvailableOrdersEvent());
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check profile completion
|
// Check profile completion
|
||||||
@@ -90,18 +92,44 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (widget.refreshAvailable) {
|
if (widget.refreshAvailable) {
|
||||||
_refreshAvailable = true;
|
|
||||||
_pendingAvailableRefresh = true;
|
_pendingAvailableRefresh = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = Translations.of(context);
|
final Translations t = Translations.of(context);
|
||||||
return BlocProvider.value(
|
return MultiBlocProvider(
|
||||||
value: _bloc,
|
providers: <BlocProvider<dynamic>>[
|
||||||
|
BlocProvider<ShiftsBloc>.value(value: _bloc),
|
||||||
|
BlocProvider<AvailableOrdersBloc>.value(value: _ordersBloc),
|
||||||
|
],
|
||||||
|
child: BlocListener<AvailableOrdersBloc, AvailableOrdersState>(
|
||||||
|
listener: (BuildContext context, AvailableOrdersState ordersState) {
|
||||||
|
// Show booking success / error snackbar.
|
||||||
|
if (ordersState.lastBooking != null) {
|
||||||
|
final OrderBooking booking = ordersState.lastBooking!;
|
||||||
|
final String message =
|
||||||
|
booking.status.toUpperCase() == 'CONFIRMED'
|
||||||
|
? t.available_orders.booking_confirmed
|
||||||
|
: t.available_orders.booking_pending;
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: message,
|
||||||
|
type: UiSnackbarType.success,
|
||||||
|
);
|
||||||
|
_ordersBloc.add(const ClearBookingResultEvent());
|
||||||
|
}
|
||||||
|
if (ordersState.errorMessage != null) {
|
||||||
|
UiSnackbar.show(
|
||||||
|
context,
|
||||||
|
message: translateErrorKey(ordersState.errorMessage!),
|
||||||
|
type: UiSnackbarType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
child: BlocConsumer<ShiftsBloc, ShiftsState>(
|
child: BlocConsumer<ShiftsBloc, ShiftsState>(
|
||||||
listener: (context, state) {
|
listener: (BuildContext context, ShiftsState state) {
|
||||||
if (state.status == ShiftsStatus.error &&
|
if (state.status == ShiftsStatus.error &&
|
||||||
state.errorMessage != null) {
|
state.errorMessage != null) {
|
||||||
UiSnackbar.show(
|
UiSnackbar.show(
|
||||||
@@ -111,30 +139,25 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (BuildContext context, ShiftsState state) {
|
||||||
if (_pendingAvailableRefresh && state.status == ShiftsStatus.loaded) {
|
if (_pendingAvailableRefresh &&
|
||||||
|
state.status == ShiftsStatus.loaded) {
|
||||||
_pendingAvailableRefresh = false;
|
_pendingAvailableRefresh = false;
|
||||||
_bloc.add(const LoadAvailableShiftsEvent(force: true));
|
_ordersBloc.add(const LoadAvailableOrdersEvent());
|
||||||
}
|
}
|
||||||
final bool baseLoaded = state.status == ShiftsStatus.loaded;
|
final bool baseLoaded = state.status == ShiftsStatus.loaded;
|
||||||
final List<AssignedShift> myShifts = state.myShifts;
|
final List<AssignedShift> myShifts = state.myShifts;
|
||||||
final List<OpenShift> availableJobs = state.availableShifts;
|
final List<PendingAssignment> pendingAssignments =
|
||||||
final bool availableLoading = state.availableLoading;
|
state.pendingShifts;
|
||||||
final bool availableLoaded = state.availableLoaded;
|
|
||||||
final List<PendingAssignment> pendingAssignments = state.pendingShifts;
|
|
||||||
final List<CancelledShift> cancelledShifts = state.cancelledShifts;
|
final List<CancelledShift> cancelledShifts = state.cancelledShifts;
|
||||||
final List<CompletedShift> historyShifts = state.historyShifts;
|
final List<CompletedShift> historyShifts = state.historyShifts;
|
||||||
final bool historyLoading = state.historyLoading;
|
final bool historyLoading = state.historyLoading;
|
||||||
final bool historyLoaded = state.historyLoaded;
|
final bool historyLoaded = state.historyLoaded;
|
||||||
final bool myShiftsLoaded = state.myShiftsLoaded;
|
final bool myShiftsLoaded = state.myShiftsLoaded;
|
||||||
final bool blockTabsForFind = _prioritizeFind && !availableLoaded;
|
|
||||||
|
|
||||||
// Note: "filteredJobs" logic moved to FindShiftsTab
|
|
||||||
// Note: Calendar logic moved to MyShiftsTab
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: <Widget>[
|
||||||
// Header (Blue)
|
// Header (Blue)
|
||||||
Container(
|
Container(
|
||||||
color: UiColors.primary,
|
color: UiColors.primary,
|
||||||
@@ -147,15 +170,25 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
spacing: UiConstants.space4,
|
spacing: UiConstants.space4,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
t.staff_shifts.title,
|
t.staff_shifts.title,
|
||||||
style: UiTypography.display1b.white,
|
style: UiTypography.display1b.white,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Tabs
|
// Tabs -- use BlocBuilder on orders bloc for count
|
||||||
Row(
|
BlocBuilder<AvailableOrdersBloc,
|
||||||
children: [
|
AvailableOrdersState>(
|
||||||
|
builder: (BuildContext context,
|
||||||
|
AvailableOrdersState ordersState) {
|
||||||
|
final bool ordersLoaded = ordersState.status ==
|
||||||
|
AvailableOrdersStatus.loaded;
|
||||||
|
final int ordersCount = ordersState.orders.length;
|
||||||
|
final bool blockTabsForFind =
|
||||||
|
_prioritizeFind && !ordersLoaded;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: <Widget>[
|
||||||
if (state.profileComplete != false)
|
if (state.profileComplete != false)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _buildTab(
|
child: _buildTab(
|
||||||
@@ -164,8 +197,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
UiIcons.calendar,
|
UiIcons.calendar,
|
||||||
myShifts.length,
|
myShifts.length,
|
||||||
showCount: myShiftsLoaded,
|
showCount: myShiftsLoaded,
|
||||||
enabled:
|
enabled: !blockTabsForFind &&
|
||||||
!blockTabsForFind &&
|
|
||||||
(state.profileComplete ?? false),
|
(state.profileComplete ?? false),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -179,8 +211,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
ShiftTabType.find,
|
ShiftTabType.find,
|
||||||
t.staff_shifts.tabs.find_work,
|
t.staff_shifts.tabs.find_work,
|
||||||
UiIcons.search,
|
UiIcons.search,
|
||||||
availableJobs.length,
|
ordersCount,
|
||||||
showCount: availableLoaded,
|
showCount: ordersLoaded,
|
||||||
enabled: baseLoaded,
|
enabled: baseLoaded,
|
||||||
),
|
),
|
||||||
if (state.profileComplete != false)
|
if (state.profileComplete != false)
|
||||||
@@ -195,8 +227,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
UiIcons.clock,
|
UiIcons.clock,
|
||||||
historyShifts.length,
|
historyShifts.length,
|
||||||
showCount: historyLoaded,
|
showCount: historyLoaded,
|
||||||
enabled:
|
enabled: !blockTabsForFind &&
|
||||||
!blockTabsForFind &&
|
|
||||||
baseLoaded &&
|
baseLoaded &&
|
||||||
(state.profileComplete ?? false),
|
(state.profileComplete ?? false),
|
||||||
),
|
),
|
||||||
@@ -204,6 +235,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
else
|
else
|
||||||
const SizedBox.shrink(),
|
const SizedBox.shrink(),
|
||||||
],
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -216,13 +249,16 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
: state.status == ShiftsStatus.error
|
: state.status == ShiftsStatus.error
|
||||||
? Center(
|
? Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(UiConstants.space5),
|
padding:
|
||||||
|
const EdgeInsets.all(UiConstants.space5),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
translateErrorKey(state.errorMessage ?? ''),
|
translateErrorKey(
|
||||||
style: UiTypography.body2r.textSecondary,
|
state.errorMessage ?? ''),
|
||||||
|
style:
|
||||||
|
UiTypography.body2r.textSecondary,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -234,9 +270,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
myShifts,
|
myShifts,
|
||||||
pendingAssignments,
|
pendingAssignments,
|
||||||
cancelledShifts,
|
cancelledShifts,
|
||||||
availableJobs,
|
|
||||||
historyShifts,
|
historyShifts,
|
||||||
availableLoading,
|
|
||||||
historyLoading,
|
historyLoading,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -245,6 +279,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,9 +288,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
List<AssignedShift> myShifts,
|
List<AssignedShift> myShifts,
|
||||||
List<PendingAssignment> pendingAssignments,
|
List<PendingAssignment> pendingAssignments,
|
||||||
List<CancelledShift> cancelledShifts,
|
List<CancelledShift> cancelledShifts,
|
||||||
List<OpenShift> availableJobs,
|
|
||||||
List<CompletedShift> historyShifts,
|
List<CompletedShift> historyShifts,
|
||||||
bool availableLoading,
|
|
||||||
bool historyLoading,
|
bool historyLoading,
|
||||||
) {
|
) {
|
||||||
switch (_activeTab) {
|
switch (_activeTab) {
|
||||||
@@ -269,12 +302,23 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
submittingShiftId: state.submittingShiftId,
|
submittingShiftId: state.submittingShiftId,
|
||||||
);
|
);
|
||||||
case ShiftTabType.find:
|
case ShiftTabType.find:
|
||||||
if (availableLoading) {
|
return BlocBuilder<AvailableOrdersBloc, AvailableOrdersState>(
|
||||||
|
builder:
|
||||||
|
(BuildContext context, AvailableOrdersState ordersState) {
|
||||||
|
if (ordersState.status == AvailableOrdersStatus.loading) {
|
||||||
return const ShiftsPageSkeleton();
|
return const ShiftsPageSkeleton();
|
||||||
}
|
}
|
||||||
return FindShiftsTab(
|
return FindShiftsTab(
|
||||||
availableJobs: availableJobs,
|
availableOrders: ordersState.orders,
|
||||||
profileComplete: state.profileComplete ?? true,
|
profileComplete: state.profileComplete ?? true,
|
||||||
|
onBook: (String orderId, String roleId) {
|
||||||
|
_ordersBloc.add(
|
||||||
|
BookOrderEvent(orderId: orderId, roleId: roleId),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
bookingInProgress: ordersState.bookingInProgress,
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
case ShiftTabType.history:
|
case ShiftTabType.history:
|
||||||
if (historyLoading) {
|
if (historyLoading) {
|
||||||
@@ -296,7 +340,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
bool showCount = true,
|
bool showCount = true,
|
||||||
bool enabled = true,
|
bool enabled = true,
|
||||||
}) {
|
}) {
|
||||||
final isActive = _activeTab == type;
|
final bool isActive = _activeTab == type;
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: !enabled
|
onTap: !enabled
|
||||||
@@ -307,7 +351,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
_bloc.add(LoadHistoryShiftsEvent());
|
_bloc.add(LoadHistoryShiftsEvent());
|
||||||
}
|
}
|
||||||
if (type == ShiftTabType.find) {
|
if (type == ShiftTabType.find) {
|
||||||
_bloc.add(LoadAvailableShiftsEvent());
|
_ordersBloc.add(const LoadAvailableOrdersEvent());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -324,7 +368,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
size: 14,
|
size: 14,
|
||||||
@@ -338,11 +382,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style:
|
style: (isActive
|
||||||
(isActive
|
? UiTypography.body3m
|
||||||
? UiTypography.body3m.copyWith(
|
.copyWith(color: UiColors.primary)
|
||||||
color: UiColors.primary,
|
|
||||||
)
|
|
||||||
: UiTypography.body3m.white)
|
: UiTypography.body3m.white)
|
||||||
.copyWith(
|
.copyWith(
|
||||||
color: !enabled
|
color: !enabled
|
||||||
@@ -352,7 +394,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showCount) ...[
|
if (showCount) ...<Widget>[
|
||||||
const SizedBox(width: UiConstants.space1),
|
const SizedBox(width: UiConstants.space1),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
@@ -368,7 +410,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
"$count",
|
'$count',
|
||||||
style: UiTypography.footnote1b.copyWith(
|
style: UiTypography.footnote1b.copyWith(
|
||||||
color: isActive ? UiColors.primary : UiColors.white,
|
color: isActive ? UiColors.primary : UiColors.white,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,415 @@
|
|||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
/// Card displaying an [AvailableOrder] from the staff marketplace.
|
||||||
|
///
|
||||||
|
/// Shows role, location, schedule, pay rate, and a booking/apply action.
|
||||||
|
class AvailableOrderCard extends StatelessWidget {
|
||||||
|
/// Creates an [AvailableOrderCard].
|
||||||
|
const AvailableOrderCard({
|
||||||
|
super.key,
|
||||||
|
required this.order,
|
||||||
|
required this.onBook,
|
||||||
|
this.bookingInProgress = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The available order to display.
|
||||||
|
final AvailableOrder order;
|
||||||
|
|
||||||
|
/// Callback when the user taps book/apply, providing orderId and roleId.
|
||||||
|
final void Function(String orderId, String roleId) onBook;
|
||||||
|
|
||||||
|
/// Whether a booking request is currently in progress.
|
||||||
|
final bool bookingInProgress;
|
||||||
|
|
||||||
|
/// Formats a date-only string (e.g. "2026-03-24") to "Mar 24".
|
||||||
|
String _formatDateShort(String dateStr) {
|
||||||
|
if (dateStr.isEmpty) return '';
|
||||||
|
try {
|
||||||
|
final DateTime date = DateTime.parse(dateStr);
|
||||||
|
return DateFormat('MMM d').format(date);
|
||||||
|
} catch (_) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a human-readable label for the order type.
|
||||||
|
String _orderTypeLabel(OrderType type) {
|
||||||
|
switch (type) {
|
||||||
|
case OrderType.oneTime:
|
||||||
|
return t.staff_shifts.filter.one_day;
|
||||||
|
case OrderType.recurring:
|
||||||
|
return t.staff_shifts.filter.multi_day;
|
||||||
|
case OrderType.permanent:
|
||||||
|
return t.staff_shifts.filter.long_term;
|
||||||
|
case OrderType.rapid:
|
||||||
|
return 'Rapid';
|
||||||
|
case OrderType.unknown:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a capitalised short label for a dispatch team value.
|
||||||
|
String _dispatchTeamLabel(String team) {
|
||||||
|
switch (team.toUpperCase()) {
|
||||||
|
case 'CORE':
|
||||||
|
return 'Core';
|
||||||
|
case 'CERTIFIED_LOCATION':
|
||||||
|
return 'Certified';
|
||||||
|
case 'MARKETPLACE':
|
||||||
|
return 'Marketplace';
|
||||||
|
default:
|
||||||
|
return team;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final AvailableOrderSchedule schedule = order.schedule;
|
||||||
|
final int spotsLeft = order.requiredWorkerCount - order.filledCount;
|
||||||
|
final String hourlyDisplay =
|
||||||
|
'\$${order.hourlyRate.toStringAsFixed(order.hourlyRate.truncateToDouble() == order.hourlyRate ? 0 : 2)}';
|
||||||
|
final String dateRange =
|
||||||
|
'${_formatDateShort(schedule.startDate)} - ${_formatDateShort(schedule.endDate)}';
|
||||||
|
final String timeRange = '${schedule.startTime} - ${schedule.endTime}';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.white,
|
||||||
|
borderRadius: UiConstants.radiusLg,
|
||||||
|
border: Border.all(color: UiColors.border),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(UiConstants.space4),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// -- Badge row: order type, instant book, dispatch team --
|
||||||
|
_buildBadgeRow(),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// -- Main content row: icon + details + pay --
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Role icon
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: <Color>[
|
||||||
|
UiColors.primary.withValues(alpha: 0.09),
|
||||||
|
UiColors.primary.withValues(alpha: 0.03),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(UiConstants.radiusBase),
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.briefcase,
|
||||||
|
color: UiColors.primary,
|
||||||
|
size: UiConstants.iconMd,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
|
||||||
|
// Details
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
// Role name + hourly rate
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
order.roleName,
|
||||||
|
style: UiTypography.body2m.textPrimary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
if (order.clientName.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
order.clientName,
|
||||||
|
style: UiTypography.body3r.textSecondary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
'$hourlyDisplay${t.available_orders.per_hour}',
|
||||||
|
style: UiTypography.title1m.textPrimary,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'${order.filledCount}/${order.requiredWorkerCount} ${t.available_orders.spots_left(count: spotsLeft)}',
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space2),
|
||||||
|
|
||||||
|
// Location
|
||||||
|
if (order.location.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(bottom: UiConstants.space1),
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.mapPin,
|
||||||
|
size: UiConstants.iconXs,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
order.location,
|
||||||
|
style: UiTypography.footnote1r.textSecondary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Address
|
||||||
|
if (order.locationAddress.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(bottom: UiConstants.space2),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: UiConstants.iconXs + UiConstants.space1,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
order.locationAddress,
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Schedule: days of week chips
|
||||||
|
if (schedule.daysOfWeek.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(bottom: UiConstants.space2),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: UiConstants.space1,
|
||||||
|
runSpacing: UiConstants.space1,
|
||||||
|
children: schedule.daysOfWeek
|
||||||
|
.map(
|
||||||
|
(DayOfWeek day) => _buildDayChip(day),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Date range + time + shifts count
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Icon(
|
||||||
|
UiIcons.calendar,
|
||||||
|
size: UiConstants.iconXs,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
dateRange,
|
||||||
|
style: UiTypography.footnote1r.textSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space3),
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: UiConstants.iconXs,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: UiConstants.space1),
|
||||||
|
Text(
|
||||||
|
timeRange,
|
||||||
|
style: UiTypography.footnote1r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
|
|
||||||
|
// Total shifts + timezone
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
Text(
|
||||||
|
t.available_orders.shifts_count(
|
||||||
|
count: schedule.totalShifts,
|
||||||
|
),
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
if (schedule.timezone.isNotEmpty) ...<Widget>[
|
||||||
|
const SizedBox(width: UiConstants.space2),
|
||||||
|
Text(
|
||||||
|
schedule.timezone,
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: UiConstants.space3),
|
||||||
|
|
||||||
|
// -- Action button --
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: bookingInProgress
|
||||||
|
? null
|
||||||
|
: () => onBook(order.orderId, order.roleId),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
foregroundColor: UiColors.white,
|
||||||
|
disabledBackgroundColor:
|
||||||
|
UiColors.primary.withValues(alpha: 0.5),
|
||||||
|
disabledForegroundColor: UiColors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius:
|
||||||
|
BorderRadius.circular(UiConstants.radiusMdValue),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: UiConstants.space3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: bookingInProgress
|
||||||
|
? const SizedBox(
|
||||||
|
width: UiConstants.iconMd,
|
||||||
|
height: UiConstants.iconMd,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: UiColors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
order.instantBook
|
||||||
|
? t.available_orders.book_order
|
||||||
|
: t.available_orders.apply,
|
||||||
|
style: UiTypography.body2m.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the horizontal row of badge chips at the top of the card.
|
||||||
|
Widget _buildBadgeRow() {
|
||||||
|
return Wrap(
|
||||||
|
spacing: UiConstants.space2,
|
||||||
|
runSpacing: UiConstants.space1,
|
||||||
|
children: <Widget>[
|
||||||
|
// Order type badge
|
||||||
|
_buildBadge(
|
||||||
|
label: _orderTypeLabel(order.orderType),
|
||||||
|
backgroundColor: UiColors.background,
|
||||||
|
textColor: UiColors.textSecondary,
|
||||||
|
borderColor: UiColors.border,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Instant book badge
|
||||||
|
if (order.instantBook)
|
||||||
|
_buildBadge(
|
||||||
|
label: t.available_orders.instant_book,
|
||||||
|
backgroundColor: UiColors.success.withValues(alpha: 0.1),
|
||||||
|
textColor: UiColors.success,
|
||||||
|
borderColor: UiColors.success.withValues(alpha: 0.3),
|
||||||
|
icon: UiIcons.zap,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Dispatch team badge
|
||||||
|
if (order.dispatchTeam.isNotEmpty)
|
||||||
|
_buildBadge(
|
||||||
|
label: _dispatchTeamLabel(order.dispatchTeam),
|
||||||
|
backgroundColor: UiColors.primary.withValues(alpha: 0.08),
|
||||||
|
textColor: UiColors.primary,
|
||||||
|
borderColor: UiColors.primary.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a single badge chip with optional leading icon.
|
||||||
|
Widget _buildBadge({
|
||||||
|
required String label,
|
||||||
|
required Color backgroundColor,
|
||||||
|
required Color textColor,
|
||||||
|
required Color borderColor,
|
||||||
|
IconData? icon,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space2,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
border: Border.all(color: borderColor),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: <Widget>[
|
||||||
|
if (icon != null) ...<Widget>[
|
||||||
|
Icon(icon, size: 10, color: textColor),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.footnote2m.copyWith(color: textColor),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a small chip showing a day-of-week abbreviation.
|
||||||
|
Widget _buildDayChip(DayOfWeek day) {
|
||||||
|
// Display as 3-letter capitalised abbreviation (e.g. "MON" -> "Mon").
|
||||||
|
final String label = day.value.isNotEmpty
|
||||||
|
? '${day.value[0]}${day.value.substring(1).toLowerCase()}'
|
||||||
|
: '';
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: UiConstants.space2,
|
||||||
|
vertical: 2,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withValues(alpha: 0.08),
|
||||||
|
borderRadius: UiConstants.radiusSm,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: UiTypography.footnote2m.copyWith(color: UiColors.primary),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,26 +2,36 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:krow_core/core.dart';
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:staff_shifts/src/presentation/widgets/available_order_card.dart';
|
||||||
import 'package:staff_shifts/src/presentation/widgets/shared/empty_state_view.dart';
|
import 'package:staff_shifts/src/presentation/widgets/shared/empty_state_view.dart';
|
||||||
|
|
||||||
/// Tab showing open shifts available for the worker to browse and apply.
|
/// Tab showing available orders for the worker to browse and book.
|
||||||
|
///
|
||||||
|
/// Replaces the former open-shift listing with order-level marketplace cards.
|
||||||
class FindShiftsTab extends StatefulWidget {
|
class FindShiftsTab extends StatefulWidget {
|
||||||
/// Creates a [FindShiftsTab].
|
/// Creates a [FindShiftsTab].
|
||||||
const FindShiftsTab({
|
const FindShiftsTab({
|
||||||
super.key,
|
super.key,
|
||||||
required this.availableJobs,
|
required this.availableOrders,
|
||||||
this.profileComplete = true,
|
this.profileComplete = true,
|
||||||
|
required this.onBook,
|
||||||
|
this.bookingInProgress = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Open shifts loaded from the V2 API.
|
/// Available orders loaded from the V2 API.
|
||||||
final List<OpenShift> availableJobs;
|
final List<AvailableOrder> availableOrders;
|
||||||
|
|
||||||
/// Whether the worker's profile is complete.
|
/// Whether the worker's profile is complete.
|
||||||
final bool profileComplete;
|
final bool profileComplete;
|
||||||
|
|
||||||
|
/// Callback when the worker taps book/apply on an order card.
|
||||||
|
final void Function(String orderId, String roleId) onBook;
|
||||||
|
|
||||||
|
/// Whether a booking request is currently in flight.
|
||||||
|
final bool bookingInProgress;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<FindShiftsTab> createState() => _FindShiftsTabState();
|
State<FindShiftsTab> createState() => _FindShiftsTabState();
|
||||||
}
|
}
|
||||||
@@ -30,18 +40,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
String _searchQuery = '';
|
String _searchQuery = '';
|
||||||
String _jobType = 'all';
|
String _jobType = 'all';
|
||||||
|
|
||||||
String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt);
|
/// Builds a filter tab chip.
|
||||||
|
|
||||||
String _formatDate(DateTime date) {
|
|
||||||
final DateTime now = DateTime.now();
|
|
||||||
final DateTime today = DateTime(now.year, now.month, now.day);
|
|
||||||
final DateTime tomorrow = today.add(const Duration(days: 1));
|
|
||||||
final DateTime d = DateTime(date.year, date.month, date.day);
|
|
||||||
if (d == today) return 'Today';
|
|
||||||
if (d == tomorrow) return 'Tomorrow';
|
|
||||||
return DateFormat('EEE, MMM d').format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildFilterTab(String id, String label) {
|
Widget _buildFilterTab(String id, String label) {
|
||||||
final bool isSelected = _jobType == id;
|
final bool isSelected = _jobType == id;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
@@ -69,178 +68,27 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<OpenShift> _filterByType(List<OpenShift> shifts) {
|
/// Filters orders by the selected order type tab.
|
||||||
if (_jobType == 'all') return shifts;
|
List<AvailableOrder> _filterByType(List<AvailableOrder> orders) {
|
||||||
return shifts.where((OpenShift s) {
|
if (_jobType == 'all') return orders;
|
||||||
if (_jobType == 'one-day') return s.orderType == OrderType.oneTime;
|
return orders.where((AvailableOrder o) {
|
||||||
if (_jobType == 'multi-day') return s.orderType == OrderType.recurring;
|
if (_jobType == 'one-day') return o.orderType == OrderType.oneTime;
|
||||||
if (_jobType == 'long-term') return s.orderType == OrderType.permanent;
|
if (_jobType == 'multi-day') return o.orderType == OrderType.recurring;
|
||||||
|
if (_jobType == 'long-term') return o.orderType == OrderType.permanent;
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds an open shift card.
|
|
||||||
Widget _buildOpenShiftCard(BuildContext context, OpenShift shift) {
|
|
||||||
final double hourlyRate = shift.hourlyRateCents / 100;
|
|
||||||
final int minutes = shift.endTime.difference(shift.startTime).inMinutes;
|
|
||||||
final double duration = minutes / 60;
|
|
||||||
final double estimatedTotal = hourlyRate * duration;
|
|
||||||
|
|
||||||
String typeLabel;
|
|
||||||
switch (shift.orderType) {
|
|
||||||
case OrderType.permanent:
|
|
||||||
typeLabel = t.staff_shifts.filter.long_term;
|
|
||||||
case OrderType.recurring:
|
|
||||||
typeLabel = t.staff_shifts.filter.multi_day;
|
|
||||||
case OrderType.oneTime:
|
|
||||||
default:
|
|
||||||
typeLabel = t.staff_shifts.filter.one_day;
|
|
||||||
}
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.white,
|
|
||||||
borderRadius: UiConstants.radiusLg,
|
|
||||||
border: Border.all(color: UiColors.border),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(UiConstants.space4),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
// Type badge
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: UiConstants.space2),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: UiConstants.space2,
|
|
||||||
vertical: 2,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.background,
|
|
||||||
borderRadius: UiConstants.radiusSm,
|
|
||||||
border: Border.all(color: UiColors.border),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
typeLabel,
|
|
||||||
style: UiTypography.footnote2m
|
|
||||||
.copyWith(color: UiColors.textSecondary),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Container(
|
|
||||||
width: 44,
|
|
||||||
height: 44,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: <Color>[
|
|
||||||
UiColors.primary.withValues(alpha: 0.09),
|
|
||||||
UiColors.primary.withValues(alpha: 0.03),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(UiConstants.radiusBase),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(UiIcons.briefcase,
|
|
||||||
color: UiColors.primary, size: UiConstants.iconMd),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: <Widget>[
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(shift.roleName,
|
|
||||||
style: UiTypography.body2m.textPrimary,
|
|
||||||
overflow: TextOverflow.ellipsis),
|
|
||||||
Text(shift.location,
|
|
||||||
style: UiTypography.body3r.textSecondary,
|
|
||||||
overflow: TextOverflow.ellipsis),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: <Widget>[
|
|
||||||
Text('\$${estimatedTotal.toStringAsFixed(0)}',
|
|
||||||
style: UiTypography.title1m.textPrimary),
|
|
||||||
Text(
|
|
||||||
'\$${hourlyRate.toInt()}/hr \u00b7 ${duration.toInt()}h',
|
|
||||||
style:
|
|
||||||
UiTypography.footnote2r.textSecondary),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space2),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(UiIcons.calendar,
|
|
||||||
size: UiConstants.iconXs,
|
|
||||||
color: UiColors.iconSecondary),
|
|
||||||
const SizedBox(width: UiConstants.space1),
|
|
||||||
Text(_formatDate(shift.date),
|
|
||||||
style: UiTypography.footnote1r.textSecondary),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
const Icon(UiIcons.clock,
|
|
||||||
size: UiConstants.iconXs,
|
|
||||||
color: UiColors.iconSecondary),
|
|
||||||
const SizedBox(width: UiConstants.space1),
|
|
||||||
Text(
|
|
||||||
'${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}',
|
|
||||||
style: UiTypography.footnote1r.textSecondary),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
Row(
|
|
||||||
children: <Widget>[
|
|
||||||
const Icon(UiIcons.mapPin,
|
|
||||||
size: UiConstants.iconXs,
|
|
||||||
color: UiColors.iconSecondary),
|
|
||||||
const SizedBox(width: UiConstants.space1),
|
|
||||||
Expanded(
|
|
||||||
child: Text(shift.location,
|
|
||||||
style: UiTypography.footnote1r.textSecondary,
|
|
||||||
overflow: TextOverflow.ellipsis),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Client-side filter by order type
|
// Client-side filter by order type and search query
|
||||||
final List<OpenShift> filteredJobs =
|
final List<AvailableOrder> filteredOrders =
|
||||||
_filterByType(widget.availableJobs).where((OpenShift s) {
|
_filterByType(widget.availableOrders).where((AvailableOrder o) {
|
||||||
if (_searchQuery.isEmpty) return true;
|
if (_searchQuery.isEmpty) return true;
|
||||||
final String q = _searchQuery.toLowerCase();
|
final String q = _searchQuery.toLowerCase();
|
||||||
return s.roleName.toLowerCase().contains(q) ||
|
return o.roleName.toLowerCase().contains(q) ||
|
||||||
s.location.toLowerCase().contains(q);
|
o.clientName.toLowerCase().contains(q) ||
|
||||||
|
o.location.toLowerCase().contains(q);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
@@ -322,7 +170,7 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: filteredJobs.isEmpty
|
child: filteredOrders.isEmpty
|
||||||
? EmptyStateView(
|
? EmptyStateView(
|
||||||
icon: UiIcons.search,
|
icon: UiIcons.search,
|
||||||
title: context.t.staff_shifts.find_shifts.no_jobs_title,
|
title: context.t.staff_shifts.find_shifts.no_jobs_title,
|
||||||
@@ -335,9 +183,12 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
const SizedBox(height: UiConstants.space5),
|
const SizedBox(height: UiConstants.space5),
|
||||||
...filteredJobs.map(
|
...filteredOrders.map(
|
||||||
(OpenShift shift) =>
|
(AvailableOrder order) => AvailableOrderCard(
|
||||||
_buildOpenShiftCard(context, shift),
|
order: order,
|
||||||
|
onBook: widget.onBook,
|
||||||
|
bookingInProgress: widget.bookingInProgress,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space32),
|
const SizedBox(height: UiConstants.space32),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import 'package:staff_shifts/src/domain/usecases/get_profile_completion_usecase.
|
|||||||
import 'package:staff_shifts/src/domain/usecases/get_shift_details_usecase.dart';
|
import 'package:staff_shifts/src/domain/usecases/get_shift_details_usecase.dart';
|
||||||
import 'package:staff_shifts/src/domain/usecases/accept_shift_usecase.dart';
|
import 'package:staff_shifts/src/domain/usecases/accept_shift_usecase.dart';
|
||||||
import 'package:staff_shifts/src/domain/usecases/decline_shift_usecase.dart';
|
import 'package:staff_shifts/src/domain/usecases/decline_shift_usecase.dart';
|
||||||
|
import 'package:staff_shifts/src/domain/usecases/book_order_usecase.dart';
|
||||||
|
import 'package:staff_shifts/src/domain/usecases/get_available_orders_usecase.dart';
|
||||||
import 'package:staff_shifts/src/domain/usecases/submit_for_approval_usecase.dart';
|
import 'package:staff_shifts/src/domain/usecases/submit_for_approval_usecase.dart';
|
||||||
|
import 'package:staff_shifts/src/presentation/blocs/available_orders/available_orders_bloc.dart';
|
||||||
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
|
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
|
||||||
import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_bloc.dart';
|
import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_bloc.dart';
|
||||||
import 'package:staff_shifts/src/presentation/utils/shift_tab_type.dart';
|
import 'package:staff_shifts/src/presentation/utils/shift_tab_type.dart';
|
||||||
@@ -49,6 +52,8 @@ class StaffShiftsModule extends Module {
|
|||||||
i.addLazySingleton(
|
i.addLazySingleton(
|
||||||
() => SubmitForApprovalUseCase(i.get<ShiftsRepositoryInterface>()),
|
() => SubmitForApprovalUseCase(i.get<ShiftsRepositoryInterface>()),
|
||||||
);
|
);
|
||||||
|
i.addLazySingleton(GetAvailableOrdersUseCase.new);
|
||||||
|
i.addLazySingleton(BookOrderUseCase.new);
|
||||||
|
|
||||||
// BLoC
|
// BLoC
|
||||||
i.add(
|
i.add(
|
||||||
@@ -72,6 +77,12 @@ class StaffShiftsModule extends Module {
|
|||||||
getProfileCompletion: i.get(),
|
getProfileCompletion: i.get(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
i.add(
|
||||||
|
() => AvailableOrdersBloc(
|
||||||
|
getAvailableOrders: i.get(),
|
||||||
|
bookOrder: i.get(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Reference in New Issue
Block a user