From 214e0d1237b3c901dc1306a50e151992fe65367f Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 22 Feb 2026 02:18:33 -0500 Subject: [PATCH] feat: Implement order details retrieval for pre-filling new order forms for reordering. --- .../packages/domain/lib/krow_domain.dart | 1 + .../lib/src/entities/orders/reorder_data.dart | 76 ++++ .../lib/src/create_order_module.dart | 26 +- .../client_create_order_repository_impl.dart | 388 ++++++++++++------ ...ent_create_order_repository_interface.dart | 5 + ...get_order_details_for_reorder_usecase.dart | 14 + .../one_time_order/one_time_order_bloc.dart | 90 +++- .../permanent_order/permanent_order_bloc.dart | 92 ++++- .../recurring_order/recurring_order_bloc.dart | 93 ++++- .../connector/shiftRole/queries.gql | 6 + 10 files changed, 594 insertions(+), 197 deletions(-) create mode 100644 apps/mobile/packages/domain/lib/src/entities/orders/reorder_data.dart create mode 100644 apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/get_order_details_for_reorder_usecase.dart diff --git a/apps/mobile/packages/domain/lib/krow_domain.dart b/apps/mobile/packages/domain/lib/krow_domain.dart index 7fcca148..15a1b2e4 100644 --- a/apps/mobile/packages/domain/lib/krow_domain.dart +++ b/apps/mobile/packages/domain/lib/krow_domain.dart @@ -42,6 +42,7 @@ export 'src/entities/orders/permanent_order.dart'; export 'src/entities/orders/permanent_order_position.dart'; export 'src/entities/orders/order_type.dart'; export 'src/entities/orders/order_item.dart'; +export 'src/entities/orders/reorder_data.dart'; // Skills & Certs export 'src/entities/skills/skill.dart'; diff --git a/apps/mobile/packages/domain/lib/src/entities/orders/reorder_data.dart b/apps/mobile/packages/domain/lib/src/entities/orders/reorder_data.dart new file mode 100644 index 00000000..2f325d3a --- /dev/null +++ b/apps/mobile/packages/domain/lib/src/entities/orders/reorder_data.dart @@ -0,0 +1,76 @@ +import 'package:equatable/equatable.dart'; +import 'one_time_order.dart'; +import 'order_type.dart'; + +/// Represents the full details of an order retrieved for reordering. +class ReorderData extends Equatable { + const ReorderData({ + required this.orderId, + required this.orderType, + required this.eventName, + required this.vendorId, + required this.hub, + required this.positions, + this.date, + this.startDate, + this.endDate, + this.recurringDays = const [], + this.permanentDays = const [], + }); + + final String orderId; + final OrderType orderType; + final String eventName; + final String? vendorId; + final OneTimeOrderHubDetails hub; + final List positions; + + // One-time specific + final DateTime? date; + + // Recurring/Permanent specific + final DateTime? startDate; + final DateTime? endDate; + final List recurringDays; + final List permanentDays; + + @override + List get props => [ + orderId, + orderType, + eventName, + vendorId, + hub, + positions, + date, + startDate, + endDate, + recurringDays, + permanentDays, + ]; +} + +class ReorderPosition extends Equatable { + const ReorderPosition({ + required this.roleId, + required this.count, + required this.startTime, + required this.endTime, + this.lunchBreak = 'NO_BREAK', + }); + + final String roleId; + final int count; + final String startTime; + final String endTime; + final String lunchBreak; + + @override + List get props => [ + roleId, + count, + startTime, + endTime, + lunchBreak, + ]; +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart index 09416ced..e459dd35 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/create_order_module.dart @@ -8,6 +8,7 @@ import 'domain/usecases/create_one_time_order_usecase.dart'; import 'domain/usecases/create_permanent_order_usecase.dart'; import 'domain/usecases/create_recurring_order_usecase.dart'; import 'domain/usecases/create_rapid_order_usecase.dart'; +import 'domain/usecases/get_order_details_for_reorder_usecase.dart'; import 'presentation/blocs/index.dart'; import 'presentation/pages/create_order_page.dart'; import 'presentation/pages/one_time_order_page.dart'; @@ -27,13 +28,16 @@ class ClientCreateOrderModule extends Module { @override void binds(Injector i) { // Repositories - i.addLazySingleton(ClientCreateOrderRepositoryImpl.new); + i.addLazySingleton( + ClientCreateOrderRepositoryImpl.new, + ); // UseCases i.addLazySingleton(CreateOneTimeOrderUseCase.new); i.addLazySingleton(CreatePermanentOrderUseCase.new); i.addLazySingleton(CreateRecurringOrderUseCase.new); i.addLazySingleton(CreateRapidOrderUseCase.new); + i.addLazySingleton(GetOrderDetailsForReorderUseCase.new); // BLoCs i.add(RapidOrderBloc.new); @@ -49,19 +53,31 @@ class ClientCreateOrderModule extends Module { child: (BuildContext context) => const ClientCreateOrderPage(), ); r.child( - ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRapid), + ClientPaths.childRoute( + ClientPaths.createOrder, + ClientPaths.createOrderRapid, + ), child: (BuildContext context) => const RapidOrderPage(), ); r.child( - ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderOneTime), + ClientPaths.childRoute( + ClientPaths.createOrder, + ClientPaths.createOrderOneTime, + ), child: (BuildContext context) => const OneTimeOrderPage(), ); r.child( - ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderRecurring), + ClientPaths.childRoute( + ClientPaths.createOrder, + ClientPaths.createOrderRecurring, + ), child: (BuildContext context) => const RecurringOrderPage(), ); r.child( - ClientPaths.childRoute(ClientPaths.createOrder, ClientPaths.createOrderPermanent), + ClientPaths.childRoute( + ClientPaths.createOrder, + ClientPaths.createOrderPermanent, + ), child: (BuildContext context) => const PermanentOrderPage(), ); } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart index 18212431..c756a555 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -1,4 +1,4 @@ -import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; +import 'package:firebase_data_connect/firebase_data_connect.dart'; import 'package:intl/intl.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; import 'package:krow_domain/krow_domain.dart' as domain; @@ -11,10 +11,10 @@ import '../../domain/repositories/client_create_order_repository_interface.dart' /// /// It follows the KROW Clean Architecture by keeping the data layer focused /// on delegation and data mapping, without business logic. -class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInterface { - ClientCreateOrderRepositoryImpl({ - required dc.DataConnectService service, - }) : _service = service; +class ClientCreateOrderRepositoryImpl + implements ClientCreateOrderRepositoryInterface { + ClientCreateOrderRepositoryImpl({required dc.DataConnectService service}) + : _service = service; final dc.DataConnectService _service; @@ -36,19 +36,19 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte order.date.month, order.date.day, ); - final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly); - final fdc.OperationResult orderResult = - await _service.connector - .createOrder( - businessId: businessId, - orderType: dc.OrderType.ONE_TIME, - teamHubId: hub.id, - ) - .vendorId(vendorId) - .eventName(order.eventName) - .status(dc.OrderStatus.POSTED) - .date(orderTimestamp) - .execute(); + final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly); + final OperationResult + orderResult = await _service.connector + .createOrder( + businessId: businessId, + orderType: dc.OrderType.ONE_TIME, + teamHubId: hub.id, + ) + .vendorId(vendorId) + .eventName(order.eventName) + .status(dc.OrderStatus.POSTED) + .date(orderTimestamp) + .execute(); final String orderId = orderResult.data.order_insert.id; @@ -59,32 +59,34 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte final String shiftTitle = 'Shift 1 ${_formatDate(order.date)}'; final double shiftCost = _calculateShiftCost(order); - final fdc.OperationResult shiftResult = - await _service.connector - .createShift(title: shiftTitle, orderId: orderId) - .date(orderTimestamp) - .location(hub.name) - .locationAddress(hub.address) - .latitude(hub.latitude) - .longitude(hub.longitude) - .placeId(hub.placeId) - .city(hub.city) - .state(hub.state) - .street(hub.street) - .country(hub.country) - .status(dc.ShiftStatus.OPEN) - .workersNeeded(workersNeeded) - .filled(0) - .durationDays(1) - .cost(shiftCost) - .execute(); + final OperationResult + shiftResult = await _service.connector + .createShift(title: shiftTitle, orderId: orderId) + .date(orderTimestamp) + .location(hub.name) + .locationAddress(hub.address) + .latitude(hub.latitude) + .longitude(hub.longitude) + .placeId(hub.placeId) + .city(hub.city) + .state(hub.state) + .street(hub.street) + .country(hub.country) + .status(dc.ShiftStatus.OPEN) + .workersNeeded(workersNeeded) + .filled(0) + .durationDays(1) + .cost(shiftCost) + .execute(); final String shiftId = shiftResult.data.shift_insert.id; for (final domain.OneTimeOrderPosition position in order.positions) { final DateTime start = _parseTime(order.date, position.startTime); final DateTime end = _parseTime(order.date, position.endTime); - final DateTime normalizedEnd = end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final DateTime normalizedEnd = end.isBefore(start) + ? end.add(const Duration(days: 1)) + : end; final double hours = normalizedEnd.difference(start).inMinutes / 60.0; final double rate = order.roleRates[position.role] ?? 0; final double totalValue = rate * hours * position.count; @@ -106,7 +108,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte await _service.connector .updateOrder(id: orderId, teamHubId: hub.id) - .shifts(fdc.AnyValue([shiftId])) + .shifts(AnyValue([shiftId])) .execute(); }); } @@ -129,74 +131,78 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte order.startDate.month, order.startDate.day, ); - final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly); - final fdc.Timestamp startTimestamp = orderTimestamp; - final fdc.Timestamp endTimestamp = _service.toTimestamp(order.endDate); + final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly); + final Timestamp startTimestamp = orderTimestamp; + final Timestamp endTimestamp = _service.toTimestamp(order.endDate); - final fdc.OperationResult orderResult = - await _service.connector - .createOrder( - businessId: businessId, - orderType: dc.OrderType.RECURRING, - teamHubId: hub.id, - ) - .vendorId(vendorId) - .eventName(order.eventName) - .status(dc.OrderStatus.POSTED) - .date(orderTimestamp) - .startDate(startTimestamp) - .endDate(endTimestamp) - .recurringDays(order.recurringDays) - .execute(); + final OperationResult + orderResult = await _service.connector + .createOrder( + businessId: businessId, + orderType: dc.OrderType.RECURRING, + teamHubId: hub.id, + ) + .vendorId(vendorId) + .eventName(order.eventName) + .status(dc.OrderStatus.POSTED) + .date(orderTimestamp) + .startDate(startTimestamp) + .endDate(endTimestamp) + .recurringDays(order.recurringDays) + .execute(); final String orderId = orderResult.data.order_insert.id; // NOTE: Recurring orders are limited to 30 days of generated shifts. // Future shifts beyond 30 days should be created by a scheduled job. final DateTime maxEndDate = orderDateOnly.add(const Duration(days: 29)); - final DateTime effectiveEndDate = - order.endDate.isAfter(maxEndDate) ? maxEndDate : order.endDate; + final DateTime effectiveEndDate = order.endDate.isAfter(maxEndDate) + ? maxEndDate + : order.endDate; final Set selectedDays = Set.from(order.recurringDays); final int workersNeeded = order.positions.fold( 0, - (int sum, domain.RecurringOrderPosition position) => sum + position.count, + (int sum, domain.RecurringOrderPosition position) => + sum + position.count, ); final double shiftCost = _calculateRecurringShiftCost(order); final List shiftIds = []; - for (DateTime day = orderDateOnly; - !day.isAfter(effectiveEndDate); - day = day.add(const Duration(days: 1))) { + for ( + DateTime day = orderDateOnly; + !day.isAfter(effectiveEndDate); + day = day.add(const Duration(days: 1)) + ) { final String dayLabel = _weekdayLabel(day); if (!selectedDays.contains(dayLabel)) { continue; } final String shiftTitle = 'Shift ${_formatDate(day)}'; - final fdc.Timestamp dayTimestamp = _service.toTimestamp( + final Timestamp dayTimestamp = _service.toTimestamp( DateTime(day.year, day.month, day.day), ); - final fdc.OperationResult shiftResult = - await _service.connector - .createShift(title: shiftTitle, orderId: orderId) - .date(dayTimestamp) - .location(hub.name) - .locationAddress(hub.address) - .latitude(hub.latitude) - .longitude(hub.longitude) - .placeId(hub.placeId) - .city(hub.city) - .state(hub.state) - .street(hub.street) - .country(hub.country) - .status(dc.ShiftStatus.OPEN) - .workersNeeded(workersNeeded) - .filled(0) - .durationDays(1) - .cost(shiftCost) - .execute(); + final OperationResult + shiftResult = await _service.connector + .createShift(title: shiftTitle, orderId: orderId) + .date(dayTimestamp) + .location(hub.name) + .locationAddress(hub.address) + .latitude(hub.latitude) + .longitude(hub.longitude) + .placeId(hub.placeId) + .city(hub.city) + .state(hub.state) + .street(hub.street) + .country(hub.country) + .status(dc.ShiftStatus.OPEN) + .workersNeeded(workersNeeded) + .filled(0) + .durationDays(1) + .cost(shiftCost) + .execute(); final String shiftId = shiftResult.data.shift_insert.id; shiftIds.add(shiftId); @@ -204,8 +210,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte for (final domain.RecurringOrderPosition position in order.positions) { final DateTime start = _parseTime(day, position.startTime); final DateTime end = _parseTime(day, position.endTime); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final DateTime normalizedEnd = end.isBefore(start) + ? end.add(const Duration(days: 1)) + : end; final double hours = normalizedEnd.difference(start).inMinutes / 60.0; final double rate = order.roleRates[position.role] ?? 0; final double totalValue = rate * hours * position.count; @@ -228,7 +235,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte await _service.connector .updateOrder(id: orderId, teamHubId: hub.id) - .shifts(fdc.AnyValue(shiftIds)) + .shifts(AnyValue(shiftIds)) .execute(); }); } @@ -251,23 +258,23 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte order.startDate.month, order.startDate.day, ); - final fdc.Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly); - final fdc.Timestamp startTimestamp = orderTimestamp; + final Timestamp orderTimestamp = _service.toTimestamp(orderDateOnly); + final Timestamp startTimestamp = orderTimestamp; - final fdc.OperationResult orderResult = - await _service.connector - .createOrder( - businessId: businessId, - orderType: dc.OrderType.PERMANENT, - teamHubId: hub.id, - ) - .vendorId(vendorId) - .eventName(order.eventName) - .status(dc.OrderStatus.POSTED) - .date(orderTimestamp) - .startDate(startTimestamp) - .permanentDays(order.permanentDays) - .execute(); + final OperationResult + orderResult = await _service.connector + .createOrder( + businessId: businessId, + orderType: dc.OrderType.PERMANENT, + teamHubId: hub.id, + ) + .vendorId(vendorId) + .eventName(order.eventName) + .status(dc.OrderStatus.POSTED) + .date(orderTimestamp) + .startDate(startTimestamp) + .permanentDays(order.permanentDays) + .execute(); final String orderId = orderResult.data.order_insert.id; @@ -283,38 +290,40 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte final double shiftCost = _calculatePermanentShiftCost(order); final List shiftIds = []; - for (DateTime day = orderDateOnly; - !day.isAfter(maxEndDate); - day = day.add(const Duration(days: 1))) { + for ( + DateTime day = orderDateOnly; + !day.isAfter(maxEndDate); + day = day.add(const Duration(days: 1)) + ) { final String dayLabel = _weekdayLabel(day); if (!selectedDays.contains(dayLabel)) { continue; } final String shiftTitle = 'Shift ${_formatDate(day)}'; - final fdc.Timestamp dayTimestamp = _service.toTimestamp( + final Timestamp dayTimestamp = _service.toTimestamp( DateTime(day.year, day.month, day.day), ); - final fdc.OperationResult shiftResult = - await _service.connector - .createShift(title: shiftTitle, orderId: orderId) - .date(dayTimestamp) - .location(hub.name) - .locationAddress(hub.address) - .latitude(hub.latitude) - .longitude(hub.longitude) - .placeId(hub.placeId) - .city(hub.city) - .state(hub.state) - .street(hub.street) - .country(hub.country) - .status(dc.ShiftStatus.OPEN) - .workersNeeded(workersNeeded) - .filled(0) - .durationDays(1) - .cost(shiftCost) - .execute(); + final OperationResult + shiftResult = await _service.connector + .createShift(title: shiftTitle, orderId: orderId) + .date(dayTimestamp) + .location(hub.name) + .locationAddress(hub.address) + .latitude(hub.latitude) + .longitude(hub.longitude) + .placeId(hub.placeId) + .city(hub.city) + .state(hub.state) + .street(hub.street) + .country(hub.country) + .status(dc.ShiftStatus.OPEN) + .workersNeeded(workersNeeded) + .filled(0) + .durationDays(1) + .cost(shiftCost) + .execute(); final String shiftId = shiftResult.data.shift_insert.id; shiftIds.add(shiftId); @@ -322,8 +331,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte for (final domain.OneTimeOrderPosition position in order.positions) { final DateTime start = _parseTime(day, position.startTime); final DateTime end = _parseTime(day, position.endTime); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final DateTime normalizedEnd = end.isBefore(start) + ? end.add(const Duration(days: 1)) + : end; final double hours = normalizedEnd.difference(start).inMinutes / 60.0; final double rate = order.roleRates[position.role] ?? 0; final double totalValue = rate * hours * position.count; @@ -346,7 +356,7 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte await _service.connector .updateOrder(id: orderId, teamHubId: hub.id) - .shifts(fdc.AnyValue(shiftIds)) + .shifts(AnyValue(shiftIds)) .execute(); }); } @@ -363,13 +373,76 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte throw UnimplementedError('Reorder functionality is not yet implemented.'); } + @override + Future getOrderDetailsForReorder(String orderId) async { + return _service.run(() async { + final String businessId = await _service.getBusinessId(); + final QueryResult< + dc.ListShiftRolesByBusinessAndOrderData, + dc.ListShiftRolesByBusinessAndOrderVariables + > + result = await _service.connector + .listShiftRolesByBusinessAndOrder( + businessId: businessId, + orderId: orderId, + ) + .execute(); + + final List shiftRoles = + result.data.shiftRoles; + + if (shiftRoles.isEmpty) { + throw Exception('Order not found or has no roles.'); + } + + final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrder order = + shiftRoles.first.shift.order; + + final domain.OrderType orderType = _mapOrderType(order.orderType); + + final dc.ListShiftRolesByBusinessAndOrderShiftRolesShiftOrderTeamHub + teamHub = order.teamHub; + + return domain.ReorderData( + orderId: orderId, + eventName: order.eventName ?? '', + vendorId: order.vendorId ?? '', + orderType: orderType, + hub: domain.OneTimeOrderHubDetails( + id: teamHub.id, + name: teamHub.hubName, + address: teamHub.address, + placeId: teamHub.placeId, + latitude: 0, // Not available in this query + longitude: 0, + ), + positions: shiftRoles.map(( + dc.ListShiftRolesByBusinessAndOrderShiftRoles role, + ) { + return domain.ReorderPosition( + roleId: role.roleId, + count: role.count, + startTime: _formatTimestamp(role.startTime), + endTime: _formatTimestamp(role.endTime), + lunchBreak: _formatBreakDuration(role.breakType), + ); + }).toList(), + startDate: order.startDate?.toDateTime(), + endDate: order.endDate?.toDateTime(), + recurringDays: order.recurringDays ?? const [], + permanentDays: order.permanentDays ?? const [], + ); + }); + } + double _calculateShiftCost(domain.OneTimeOrder order) { double total = 0; for (final domain.OneTimeOrderPosition position in order.positions) { final DateTime start = _parseTime(order.date, position.startTime); final DateTime end = _parseTime(order.date, position.endTime); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final DateTime normalizedEnd = end.isBefore(start) + ? end.add(const Duration(days: 1)) + : end; final double hours = normalizedEnd.difference(start).inMinutes / 60.0; final double rate = order.roleRates[position.role] ?? 0; total += rate * hours * position.count; @@ -382,8 +455,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte for (final domain.RecurringOrderPosition position in order.positions) { final DateTime start = _parseTime(order.startDate, position.startTime); final DateTime end = _parseTime(order.startDate, position.endTime); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final DateTime normalizedEnd = end.isBefore(start) + ? end.add(const Duration(days: 1)) + : end; final double hours = normalizedEnd.difference(start).inMinutes / 60.0; final double rate = order.roleRates[position.role] ?? 0; total += rate * hours * position.count; @@ -396,8 +470,9 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte for (final domain.OneTimeOrderPosition position in order.positions) { final DateTime start = _parseTime(order.startDate, position.startTime); final DateTime end = _parseTime(order.startDate, position.endTime); - final DateTime normalizedEnd = - end.isBefore(start) ? end.add(const Duration(days: 1)) : end; + final DateTime normalizedEnd = end.isBefore(start) + ? end.add(const Duration(days: 1)) + : end; final double hours = normalizedEnd.difference(start).inMinutes / 60.0; final double rate = order.roleRates[position.role] ?? 0; total += rate * hours * position.count; @@ -473,4 +548,49 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte final String day = dateTime.day.toString().padLeft(2, '0'); return '$year-$month-$day'; } + + String _formatTimestamp(Timestamp? value) { + if (value == null) return ''; + try { + return DateFormat('HH:mm').format(value.toDateTime()); + } catch (_) { + return ''; + } + } + + String _formatBreakDuration(dc.EnumValue? breakType) { + if (breakType is dc.Known) { + switch (breakType.value) { + case dc.BreakDuration.MIN_10: + return 'MIN_10'; + case dc.BreakDuration.MIN_15: + return 'MIN_15'; + case dc.BreakDuration.MIN_30: + return 'MIN_30'; + case dc.BreakDuration.MIN_45: + return 'MIN_45'; + case dc.BreakDuration.MIN_60: + return 'MIN_60'; + case dc.BreakDuration.NO_BREAK: + return 'NO_BREAK'; + } + } + return 'NO_BREAK'; + } + + domain.OrderType _mapOrderType(dc.EnumValue? orderType) { + if (orderType is dc.Known) { + switch (orderType.value) { + case dc.OrderType.ONE_TIME: + return domain.OrderType.oneTime; + case dc.OrderType.RECURRING: + return domain.OrderType.recurring; + case dc.OrderType.PERMANENT: + return domain.OrderType.permanent; + case dc.OrderType.RAPID: + return domain.OrderType.oneTime; + } + } + return domain.OrderType.oneTime; + } } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart index 3605ad41..a2c80cd5 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/repositories/client_create_order_repository_interface.dart @@ -29,4 +29,9 @@ abstract interface class ClientCreateOrderRepositoryInterface { /// [previousOrderId] is the ID of the order to reorder. /// [newDate] is the new date for the order. Future reorder(String previousOrderId, DateTime newDate); + + /// Fetches the details of an existing order to be used as a template for a new order. + /// + /// returns [ReorderData] containing the order details and positions. + Future getOrderDetailsForReorder(String orderId); } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/get_order_details_for_reorder_usecase.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/get_order_details_for_reorder_usecase.dart new file mode 100644 index 00000000..9490ccb5 --- /dev/null +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/domain/usecases/get_order_details_for_reorder_usecase.dart @@ -0,0 +1,14 @@ +import 'package:krow_core/core.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../repositories/client_create_order_repository_interface.dart'; + +/// Use case for fetching order details for reordering. +class GetOrderDetailsForReorderUseCase implements UseCase { + const GetOrderDetailsForReorderUseCase(this._repository); + final ClientCreateOrderRepositoryInterface _repository; + + @override + Future call(String orderId) { + return _repository.getOrderDetailsForReorder(orderId); + } +} diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_bloc.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_bloc.dart index 625a2057..9b9891b4 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_bloc.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_bloc.dart @@ -1,6 +1,7 @@ import 'package:client_create_order/src/domain/arguments/one_time_order_arguments.dart'; import 'package:client_create_order/src/domain/usecases/create_one_time_order_usecase.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; @@ -14,8 +15,11 @@ class OneTimeOrderBloc extends Bloc with BlocErrorHandler, SafeBloc { - OneTimeOrderBloc(this._createOneTimeOrderUseCase, this._service) - : super(OneTimeOrderState.initial()) { + OneTimeOrderBloc( + this._createOneTimeOrderUseCase, + this._getOrderDetailsForReorderUseCase, + this._service, + ) : super(OneTimeOrderState.initial()) { on(_onVendorsLoaded); on(_onVendorChanged); on(_onHubsLoaded); @@ -32,12 +36,13 @@ class OneTimeOrderBloc extends Bloc _loadHubs(); } final CreateOneTimeOrderUseCase _createOneTimeOrderUseCase; + final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase; final dc.DataConnectService _service; Future _loadVendors() async { final List? vendors = await handleErrorWithResult( action: () async { - final QueryResult result = await _service + final fdc.QueryResult result = await _service .connector .listVendors() .execute(); @@ -65,7 +70,7 @@ class OneTimeOrderBloc extends Bloc ) async { final List? roles = await handleErrorWithResult( action: () async { - final QueryResult< + final fdc.QueryResult< dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables > @@ -95,7 +100,7 @@ class OneTimeOrderBloc extends Bloc final List? hubs = await handleErrorWithResult( action: () async { final String businessId = await _service.getBusinessId(); - final QueryResult< + final fdc.QueryResult< dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables > @@ -274,27 +279,72 @@ class OneTimeOrderBloc extends Bloc ); } - void _onInitialized( + Future _onInitialized( OneTimeOrderInitialized event, Emitter emit, - ) { + ) async { final Map data = event.data; final String title = data['title']?.toString() ?? ''; - final int workers = (data['workers'] as int?) ?? 1; final DateTime? startDate = data['startDate'] as DateTime?; + final String? orderId = data['orderId']?.toString(); - emit( - state.copyWith( - eventName: title, - date: startDate ?? DateTime.now(), - positions: [ - OneTimeOrderPosition( - role: data['roleName']?.toString() ?? '', - count: workers, - startTime: data['startTime']?.toString() ?? '09:00', - endTime: data['endTime']?.toString() ?? '17:00', + emit(state.copyWith(eventName: title, date: startDate ?? DateTime.now())); + + if (orderId == null || orderId.isEmpty) return; + + emit(state.copyWith(status: OneTimeOrderStatus.loading)); + + await handleError( + emit: emit.call, + action: () async { + final ReorderData orderDetails = + await _getOrderDetailsForReorderUseCase(orderId); + + // Map positions + final List positions = orderDetails.positions.map( + (ReorderPosition role) { + return OneTimeOrderPosition( + role: role.roleId, + count: role.count, + startTime: role.startTime, + endTime: role.endTime, + lunchBreak: role.lunchBreak, + ); + }, + ).toList(); + + // Update state with order details + final Vendor? selectedVendor = state.vendors + .where((Vendor v) => v.id == orderDetails.vendorId) + .firstOrNull; + + final OneTimeOrderHubOption? selectedHub = state.hubs + .where( + (OneTimeOrderHubOption h) => + h.placeId == orderDetails.hub.placeId, + ) + .firstOrNull; + + emit( + state.copyWith( + eventName: orderDetails.eventName.isNotEmpty + ? orderDetails.eventName + : title, + positions: positions, + selectedVendor: selectedVendor, + selectedHub: selectedHub, + location: selectedHub?.name ?? '', + status: OneTimeOrderStatus.initial, ), - ], + ); + + if (selectedVendor != null) { + await _loadRolesForVendor(selectedVendor.id, emit); + } + }, + onError: (String errorKey) => state.copyWith( + status: OneTimeOrderStatus.failure, + errorMessage: errorKey, ), ); } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_bloc.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_bloc.dart index afd1ff92..6f173604 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_bloc.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_bloc.dart @@ -1,5 +1,6 @@ import 'package:client_create_order/src/domain/usecases/create_permanent_order_usecase.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; @@ -13,8 +14,11 @@ class PermanentOrderBloc extends Bloc with BlocErrorHandler, SafeBloc { - PermanentOrderBloc(this._createPermanentOrderUseCase, this._service) - : super(PermanentOrderState.initial()) { + PermanentOrderBloc( + this._createPermanentOrderUseCase, + this._getOrderDetailsForReorderUseCase, + this._service, + ) : super(PermanentOrderState.initial()) { on(_onVendorsLoaded); on(_onVendorChanged); on(_onHubsLoaded); @@ -33,6 +37,7 @@ class PermanentOrderBloc extends Bloc } final CreatePermanentOrderUseCase _createPermanentOrderUseCase; + final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase; final dc.DataConnectService _service; static const List _dayLabels = [ @@ -48,7 +53,7 @@ class PermanentOrderBloc extends Bloc Future _loadVendors() async { final List? vendors = await handleErrorWithResult( action: () async { - final QueryResult result = await _service + final fdc.QueryResult result = await _service .connector .listVendors() .execute(); @@ -76,7 +81,7 @@ class PermanentOrderBloc extends Bloc ) async { final List? roles = await handleErrorWithResult( action: () async { - final QueryResult< + final fdc.QueryResult< dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables > @@ -106,7 +111,7 @@ class PermanentOrderBloc extends Bloc final List? hubs = await handleErrorWithResult( action: () async { final String businessId = await _service.getBusinessId(); - final QueryResult< + final fdc.QueryResult< dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables > @@ -337,27 +342,76 @@ class PermanentOrderBloc extends Bloc ); } - void _onInitialized( + Future _onInitialized( PermanentOrderInitialized event, Emitter emit, - ) { + ) async { final Map data = event.data; final String title = data['title']?.toString() ?? ''; - final int workers = (data['workers'] as int?) ?? 1; final DateTime? startDate = data['startDate'] as DateTime?; + final String? orderId = data['orderId']?.toString(); emit( - state.copyWith( - eventName: title, - startDate: startDate ?? DateTime.now(), - positions: [ - PermanentOrderPosition( - role: data['roleName']?.toString() ?? '', - count: workers, - startTime: data['startTime']?.toString() ?? '09:00', - endTime: data['endTime']?.toString() ?? '17:00', + state.copyWith(eventName: title, startDate: startDate ?? DateTime.now()), + ); + + if (orderId == null || orderId.isEmpty) return; + + emit(state.copyWith(status: PermanentOrderStatus.loading)); + + await handleError( + emit: emit.call, + action: () async { + final domain.ReorderData orderDetails = + await _getOrderDetailsForReorderUseCase(orderId); + + // Map positions + final List positions = orderDetails.positions + .map((domain.ReorderPosition role) { + return PermanentOrderPosition( + role: role.roleId, + count: role.count, + startTime: role.startTime, + endTime: role.endTime, + lunchBreak: role.lunchBreak, + ); + }) + .toList(); + + // Update state with order details + final domain.Vendor? selectedVendor = state.vendors + .where((domain.Vendor v) => v.id == orderDetails.vendorId) + .firstOrNull; + + final PermanentOrderHubOption? selectedHub = state.hubs + .where( + (PermanentOrderHubOption h) => + h.placeId == orderDetails.hub.placeId, + ) + .firstOrNull; + + emit( + state.copyWith( + eventName: orderDetails.eventName.isNotEmpty + ? orderDetails.eventName + : title, + positions: positions, + selectedVendor: selectedVendor, + selectedHub: selectedHub, + location: selectedHub?.name ?? '', + status: PermanentOrderStatus.initial, + startDate: startDate ?? orderDetails.startDate ?? DateTime.now(), + permanentDays: orderDetails.permanentDays, ), - ], + ); + + if (selectedVendor != null) { + await _loadRolesForVendor(selectedVendor.id, emit); + } + }, + onError: (String errorKey) => state.copyWith( + status: PermanentOrderStatus.failure, + errorMessage: errorKey, ), ); } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_bloc.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_bloc.dart index bc71bd68..0673531e 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_bloc.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_bloc.dart @@ -1,5 +1,6 @@ import 'package:client_create_order/src/domain/usecases/create_recurring_order_usecase.dart'; -import 'package:firebase_data_connect/firebase_data_connect.dart'; +import 'package:client_create_order/src/domain/usecases/get_order_details_for_reorder_usecase.dart'; +import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:krow_core/core.dart'; import 'package:krow_data_connect/krow_data_connect.dart' as dc; @@ -13,8 +14,11 @@ class RecurringOrderBloc extends Bloc with BlocErrorHandler, SafeBloc { - RecurringOrderBloc(this._createRecurringOrderUseCase, this._service) - : super(RecurringOrderState.initial()) { + RecurringOrderBloc( + this._createRecurringOrderUseCase, + this._getOrderDetailsForReorderUseCase, + this._service, + ) : super(RecurringOrderState.initial()) { on(_onVendorsLoaded); on(_onVendorChanged); on(_onHubsLoaded); @@ -34,6 +38,7 @@ class RecurringOrderBloc extends Bloc } final CreateRecurringOrderUseCase _createRecurringOrderUseCase; + final GetOrderDetailsForReorderUseCase _getOrderDetailsForReorderUseCase; final dc.DataConnectService _service; static const List _dayLabels = [ @@ -49,7 +54,7 @@ class RecurringOrderBloc extends Bloc Future _loadVendors() async { final List? vendors = await handleErrorWithResult( action: () async { - final QueryResult result = await _service + final fdc.QueryResult result = await _service .connector .listVendors() .execute(); @@ -77,7 +82,7 @@ class RecurringOrderBloc extends Bloc ) async { final List? roles = await handleErrorWithResult( action: () async { - final QueryResult< + final fdc.QueryResult< dc.ListRolesByVendorIdData, dc.ListRolesByVendorIdVariables > @@ -107,7 +112,7 @@ class RecurringOrderBloc extends Bloc final List? hubs = await handleErrorWithResult( action: () async { final String businessId = await _service.getBusinessId(); - final QueryResult< + final fdc.QueryResult< dc.ListTeamHubsByOwnerIdData, dc.ListTeamHubsByOwnerIdVariables > @@ -356,27 +361,77 @@ class RecurringOrderBloc extends Bloc ); } - void _onInitialized( + Future _onInitialized( RecurringOrderInitialized event, Emitter emit, - ) { + ) async { final Map data = event.data; final String title = data['title']?.toString() ?? ''; - final int workers = (data['workers'] as int?) ?? 1; final DateTime? startDate = data['startDate'] as DateTime?; + final String? orderId = data['orderId']?.toString(); emit( - state.copyWith( - eventName: title, - startDate: startDate ?? DateTime.now(), - positions: [ - RecurringOrderPosition( - role: data['roleName']?.toString() ?? '', - count: workers, - startTime: data['startTime']?.toString() ?? '09:00', - endTime: data['endTime']?.toString() ?? '17:00', + state.copyWith(eventName: title, startDate: startDate ?? DateTime.now()), + ); + + if (orderId == null || orderId.isEmpty) return; + + emit(state.copyWith(status: RecurringOrderStatus.loading)); + + await handleError( + emit: emit.call, + action: () async { + final domain.ReorderData orderDetails = + await _getOrderDetailsForReorderUseCase(orderId); + + // Map positions + final List positions = orderDetails.positions + .map((domain.ReorderPosition role) { + return RecurringOrderPosition( + role: role.roleId, + count: role.count, + startTime: role.startTime, + endTime: role.endTime, + lunchBreak: role.lunchBreak, + ); + }) + .toList(); + + // Update state with order details + final domain.Vendor? selectedVendor = state.vendors + .where((domain.Vendor v) => v.id == orderDetails.vendorId) + .firstOrNull; + + final RecurringOrderHubOption? selectedHub = state.hubs + .where( + (RecurringOrderHubOption h) => + h.placeId == orderDetails.hub.placeId, + ) + .firstOrNull; + + emit( + state.copyWith( + eventName: orderDetails.eventName.isNotEmpty + ? orderDetails.eventName + : title, + positions: positions, + selectedVendor: selectedVendor, + selectedHub: selectedHub, + location: selectedHub?.name ?? '', + status: RecurringOrderStatus.initial, + startDate: startDate ?? orderDetails.startDate ?? DateTime.now(), + endDate: orderDetails.endDate ?? DateTime.now(), + recurringDays: orderDetails.recurringDays, ), - ], + ); + + if (selectedVendor != null) { + await _loadRolesForVendor(selectedVendor.id, emit); + } + }, + onError: (String errorKey) => state.copyWith( + status: RecurringOrderStatus.failure, + errorMessage: errorKey, ), ); } diff --git a/backend/dataconnect/connector/shiftRole/queries.gql b/backend/dataconnect/connector/shiftRole/queries.gql index 6795e79d..664739c9 100644 --- a/backend/dataconnect/connector/shiftRole/queries.gql +++ b/backend/dataconnect/connector/shiftRole/queries.gql @@ -404,9 +404,15 @@ query listShiftRolesByBusinessAndOrder( vendorId eventName date + startDate + endDate + recurringDays + permanentDays + orderType #location teamHub { + id address placeId hubName