From b24096eec29454a9fdf82a05f1a6dd252d842930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Salazar?= <73718835+joshrs23@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:32:09 -0500 Subject: [PATCH] recurring v2 --- .../client_create_order_repository_impl.dart | 129 ++++++++++++------ .../blocs/recurring_order_bloc.dart | 64 ++++++--- .../blocs/recurring_order_state.dart | 22 ++- .../recurring_order/recurring_order_view.dart | 27 +++- 4 files changed, 173 insertions(+), 69 deletions(-) diff --git a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart index 3ed4a088..af17ae39 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/data/repositories_impl/client_create_order_repository_impl.dart @@ -179,61 +179,84 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte 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 Set selectedDays = Set.from(order.recurringDays); final int workersNeeded = order.positions.fold( 0, (int sum, domain.RecurringOrderPosition position) => sum + position.count, ); - final String shiftTitle = 'Shift 1 ${_formatDate(order.startDate)}'; final double shiftCost = _calculateRecurringShiftCost(order); - final fdc.OperationResult shiftResult = + final List shiftIds = []; + 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( + 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.PENDING) + .workersNeeded(workersNeeded) + .filled(0) + .durationDays(1) + .cost(shiftCost) + .execute(); + + final String shiftId = shiftResult.data.shift_insert.id; + shiftIds.add(shiftId); + + 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 double hours = normalizedEnd.difference(start).inMinutes / 60.0; + final double rate = order.roleRates[position.role] ?? 0; + final double totalValue = rate * hours * position.count; + 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.PENDING) - .workersNeeded(workersNeeded) - .filled(0) - .durationDays(1) - .cost(shiftCost) + .createShiftRole( + shiftId: shiftId, + roleId: position.role, + count: position.count, + ) + .startTime(_service.toTimestamp(start)) + .endTime(_service.toTimestamp(normalizedEnd)) + .hours(hours) + .breakType(_breakDurationFromValue(position.lunchBreak)) + .isBreakPaid(_isBreakPaid(position.lunchBreak)) + .totalValue(totalValue) .execute(); - - final String shiftId = shiftResult.data.shift_insert.id; - - 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 double hours = normalizedEnd.difference(start).inMinutes / 60.0; - final double rate = order.roleRates[position.role] ?? 0; - final double totalValue = rate * hours * position.count; - - await _service.connector - .createShiftRole( - shiftId: shiftId, - roleId: position.role, - count: position.count, - ) - .startTime(_service.toTimestamp(start)) - .endTime(_service.toTimestamp(normalizedEnd)) - .hours(hours) - .breakType(_breakDurationFromValue(position.lunchBreak)) - .isBreakPaid(_isBreakPaid(position.lunchBreak)) - .totalValue(totalValue) - .execute(); + } } await _service.connector .updateOrder(id: orderId, teamHubId: hub.id) - .shifts(fdc.AnyValue([shiftId])) + .shifts(fdc.AnyValue(shiftIds)) .execute(); }); } @@ -272,6 +295,26 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte return total; } + String _weekdayLabel(DateTime date) { + switch (date.weekday) { + case DateTime.monday: + return 'MON'; + case DateTime.tuesday: + return 'TUE'; + case DateTime.wednesday: + return 'WED'; + case DateTime.thursday: + return 'THU'; + case DateTime.friday: + return 'FRI'; + case DateTime.saturday: + return 'SAT'; + case DateTime.sunday: + default: + return 'SUN'; + } + } + dc.BreakDuration _breakDurationFromValue(String value) { switch (value) { case 'MIN_10': diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_bloc.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_bloc.dart index e58178b9..b94ed6c1 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_bloc.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_bloc.dart @@ -34,13 +34,13 @@ class RecurringOrderBloc extends Bloc final dc.DataConnectService _service; static const List _dayLabels = [ - 'S', - 'M', - 'T', - 'W', - 'T', - 'F', - 'S', + 'SUN', + 'MON', + 'TUE', + 'WED', + 'THU', + 'FRI', + 'SAT', ]; Future _loadVendors() async { @@ -195,7 +195,26 @@ class RecurringOrderBloc extends Bloc if (endDate.isBefore(event.date)) { endDate = event.date; } - emit(state.copyWith(startDate: event.date, endDate: endDate)); + final int newDayIndex = event.date.weekday % 7; + final int? autoIndex = state.autoSelectedDayIndex; + List days = List.from(state.recurringDays); + if (autoIndex != null) { + final String oldDay = _dayLabels[autoIndex]; + days.remove(oldDay); + final String newDay = _dayLabels[newDayIndex]; + if (!days.contains(newDay)) { + days.add(newDay); + } + days = _sortDays(days); + } + emit( + state.copyWith( + startDate: event.date, + endDate: endDate, + recurringDays: days, + autoSelectedDayIndex: autoIndex == null ? null : newDayIndex, + ), + ); } void _onEndDateChanged( @@ -213,14 +232,18 @@ class RecurringOrderBloc extends Bloc RecurringOrderDayToggled event, Emitter emit, ) { - final List days = List.from(state.recurringDays); - if (days.contains(event.dayIndex)) { - days.remove(event.dayIndex); + final List days = List.from(state.recurringDays); + final String label = _dayLabels[event.dayIndex]; + int? autoIndex = state.autoSelectedDayIndex; + if (days.contains(label)) { + days.remove(label); + if (autoIndex == event.dayIndex) { + autoIndex = null; + } } else { - days.add(event.dayIndex); - days.sort(); + days.add(label); } - emit(state.copyWith(recurringDays: days)); + emit(state.copyWith(recurringDays: _sortDays(days), autoSelectedDayIndex: autoIndex)); } void _onPositionAdded( @@ -277,13 +300,10 @@ class RecurringOrderBloc extends Bloc if (selectedHub == null) { throw domain.OrderMissingHubException(); } - final List recurringDays = state.recurringDays - .map((int index) => _dayLabels[index]) - .toList(); final domain.RecurringOrder order = domain.RecurringOrder( startDate: state.startDate, endDate: state.endDate, - recurringDays: recurringDays, + recurringDays: state.recurringDays, location: selectedHub.name, positions: state.positions .map( @@ -325,4 +345,12 @@ class RecurringOrderBloc extends Bloc ), ); } + + static List _sortDays(List days) { + days.sort( + (String a, String b) => + _dayLabels.indexOf(a).compareTo(_dayLabels.indexOf(b)), + ); + return days; + } } diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_state.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_state.dart index f76009c1..626beae8 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_state.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/blocs/recurring_order_state.dart @@ -11,6 +11,7 @@ class RecurringOrderState extends Equatable { required this.location, required this.eventName, required this.positions, + required this.autoSelectedDayIndex, this.status = RecurringOrderStatus.initial, this.errorMessage, this.vendors = const [], @@ -23,15 +24,26 @@ class RecurringOrderState extends Equatable { factory RecurringOrderState.initial() { final DateTime now = DateTime.now(); final DateTime start = DateTime(now.year, now.month, now.day); + final List dayLabels = [ + 'SUN', + 'MON', + 'TUE', + 'WED', + 'THU', + 'FRI', + 'SAT', + ]; + final int weekdayIndex = now.weekday % 7; return RecurringOrderState( startDate: start, endDate: start.add(const Duration(days: 7)), - recurringDays: const [], + recurringDays: [dayLabels[weekdayIndex]], location: '', eventName: '', positions: const [ RecurringOrderPosition(role: '', count: 1, startTime: '', endTime: ''), ], + autoSelectedDayIndex: weekdayIndex, vendors: const [], hubs: const [], roles: const [], @@ -40,10 +52,11 @@ class RecurringOrderState extends Equatable { final DateTime startDate; final DateTime endDate; - final List recurringDays; + final List recurringDays; final String location; final String eventName; final List positions; + final int? autoSelectedDayIndex; final RecurringOrderStatus status; final String? errorMessage; final List vendors; @@ -55,10 +68,11 @@ class RecurringOrderState extends Equatable { RecurringOrderState copyWith({ DateTime? startDate, DateTime? endDate, - List? recurringDays, + List? recurringDays, String? location, String? eventName, List? positions, + int? autoSelectedDayIndex, RecurringOrderStatus? status, String? errorMessage, List? vendors, @@ -74,6 +88,7 @@ class RecurringOrderState extends Equatable { location: location ?? this.location, eventName: eventName ?? this.eventName, positions: positions ?? this.positions, + autoSelectedDayIndex: autoSelectedDayIndex ?? this.autoSelectedDayIndex, status: status ?? this.status, errorMessage: errorMessage ?? this.errorMessage, vendors: vendors ?? this.vendors, @@ -109,6 +124,7 @@ class RecurringOrderState extends Equatable { location, eventName, positions, + autoSelectedDayIndex, status, errorMessage, vendors, diff --git a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart index a5ab33eb..89a20519 100644 --- a/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart +++ b/apps/mobile/packages/features/client/create_order/lib/src/presentation/widgets/recurring_order/recurring_order_view.dart @@ -324,16 +324,33 @@ class _RecurringDaysSelector extends StatelessWidget { required this.onToggle, }); - final List selectedDays; + final List selectedDays; final ValueChanged onToggle; @override Widget build(BuildContext context) { - const List labels = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; + const List labelsShort = [ + 'S', + 'M', + 'T', + 'W', + 'T', + 'F', + 'S', + ]; + const List labelsLong = [ + 'SUN', + 'MON', + 'TUE', + 'WED', + 'THU', + 'FRI', + 'SAT', + ]; return Wrap( spacing: UiConstants.space2, - children: List.generate(labels.length, (int index) { - final bool isSelected = selectedDays.contains(index); + children: List.generate(labelsShort.length, (int index) { + final bool isSelected = selectedDays.contains(labelsLong[index]); return GestureDetector( onTap: () => onToggle(index), child: Container( @@ -346,7 +363,7 @@ class _RecurringDaysSelector extends StatelessWidget { ), alignment: Alignment.center, child: Text( - labels[index], + labelsShort[index], style: UiTypography.body2m.copyWith( color: isSelected ? UiColors.white : UiColors.textSecondary, ),