recurring v2

This commit is contained in:
José Salazar
2026-02-17 18:32:09 -05:00
parent 85c8a09d9e
commit b24096eec2
4 changed files with 173 additions and 69 deletions

View File

@@ -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<String> selectedDays = Set<String>.from(order.recurringDays);
final int workersNeeded = order.positions.fold<int>(
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<dc.CreateShiftData, dc.CreateShiftVariables> shiftResult =
final List<String> shiftIds = <String>[];
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<dc.CreateShiftData, dc.CreateShiftVariables> 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(<String>[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':

View File

@@ -34,13 +34,13 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
final dc.DataConnectService _service;
static const List<String> _dayLabels = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
Future<void> _loadVendors() async {
@@ -195,7 +195,26 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
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<String> days = List<String>.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<RecurringOrderEvent, RecurringOrderState>
RecurringOrderDayToggled event,
Emitter<RecurringOrderState> emit,
) {
final List<int> days = List<int>.from(state.recurringDays);
if (days.contains(event.dayIndex)) {
days.remove(event.dayIndex);
final List<String> days = List<String>.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<RecurringOrderEvent, RecurringOrderState>
if (selectedHub == null) {
throw domain.OrderMissingHubException();
}
final List<String> 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<RecurringOrderEvent, RecurringOrderState>
),
);
}
static List<String> _sortDays(List<String> days) {
days.sort(
(String a, String b) =>
_dayLabels.indexOf(a).compareTo(_dayLabels.indexOf(b)),
);
return days;
}
}

View File

@@ -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 <Vendor>[],
@@ -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<String> dayLabels = <String>[
'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 <int>[],
recurringDays: <String>[dayLabels[weekdayIndex]],
location: '',
eventName: '',
positions: const <RecurringOrderPosition>[
RecurringOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
],
autoSelectedDayIndex: weekdayIndex,
vendors: const <Vendor>[],
hubs: const <RecurringOrderHubOption>[],
roles: const <RecurringOrderRoleOption>[],
@@ -40,10 +52,11 @@ class RecurringOrderState extends Equatable {
final DateTime startDate;
final DateTime endDate;
final List<int> recurringDays;
final List<String> recurringDays;
final String location;
final String eventName;
final List<RecurringOrderPosition> positions;
final int? autoSelectedDayIndex;
final RecurringOrderStatus status;
final String? errorMessage;
final List<Vendor> vendors;
@@ -55,10 +68,11 @@ class RecurringOrderState extends Equatable {
RecurringOrderState copyWith({
DateTime? startDate,
DateTime? endDate,
List<int>? recurringDays,
List<String>? recurringDays,
String? location,
String? eventName,
List<RecurringOrderPosition>? positions,
int? autoSelectedDayIndex,
RecurringOrderStatus? status,
String? errorMessage,
List<Vendor>? 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,

View File

@@ -324,16 +324,33 @@ class _RecurringDaysSelector extends StatelessWidget {
required this.onToggle,
});
final List<int> selectedDays;
final List<String> selectedDays;
final ValueChanged<int> onToggle;
@override
Widget build(BuildContext context) {
const List<String> labels = <String>['S', 'M', 'T', 'W', 'T', 'F', 'S'];
const List<String> labelsShort = <String>[
'S',
'M',
'T',
'W',
'T',
'F',
'S',
];
const List<String> labelsLong = <String>[
'SUN',
'MON',
'TUE',
'WED',
'THU',
'FRI',
'SAT',
];
return Wrap(
spacing: UiConstants.space2,
children: List<Widget>.generate(labels.length, (int index) {
final bool isSelected = selectedDays.contains(index);
children: List<Widget>.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,
),