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; 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>( final int workersNeeded = order.positions.fold<int>(
0, 0,
(int sum, domain.RecurringOrderPosition position) => sum + position.count, (int sum, domain.RecurringOrderPosition position) => sum + position.count,
); );
final String shiftTitle = 'Shift 1 ${_formatDate(order.startDate)}';
final double shiftCost = _calculateRecurringShiftCost(order); 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 await _service.connector
.createShift(title: shiftTitle, orderId: orderId) .createShiftRole(
.date(orderTimestamp) shiftId: shiftId,
.location(hub.name) roleId: position.role,
.locationAddress(hub.address) count: position.count,
.latitude(hub.latitude) )
.longitude(hub.longitude) .startTime(_service.toTimestamp(start))
.placeId(hub.placeId) .endTime(_service.toTimestamp(normalizedEnd))
.city(hub.city) .hours(hours)
.state(hub.state) .breakType(_breakDurationFromValue(position.lunchBreak))
.street(hub.street) .isBreakPaid(_isBreakPaid(position.lunchBreak))
.country(hub.country) .totalValue(totalValue)
.status(dc.ShiftStatus.PENDING)
.workersNeeded(workersNeeded)
.filled(0)
.durationDays(1)
.cost(shiftCost)
.execute(); .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 await _service.connector
.updateOrder(id: orderId, teamHubId: hub.id) .updateOrder(id: orderId, teamHubId: hub.id)
.shifts(fdc.AnyValue(<String>[shiftId])) .shifts(fdc.AnyValue(shiftIds))
.execute(); .execute();
}); });
} }
@@ -272,6 +295,26 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
return total; 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) { dc.BreakDuration _breakDurationFromValue(String value) {
switch (value) { switch (value) {
case 'MIN_10': case 'MIN_10':

View File

@@ -34,13 +34,13 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
final dc.DataConnectService _service; final dc.DataConnectService _service;
static const List<String> _dayLabels = <String>[ static const List<String> _dayLabels = <String>[
'S', 'SUN',
'M', 'MON',
'T', 'TUE',
'W', 'WED',
'T', 'THU',
'F', 'FRI',
'S', 'SAT',
]; ];
Future<void> _loadVendors() async { Future<void> _loadVendors() async {
@@ -195,7 +195,26 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
if (endDate.isBefore(event.date)) { if (endDate.isBefore(event.date)) {
endDate = 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( void _onEndDateChanged(
@@ -213,14 +232,18 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
RecurringOrderDayToggled event, RecurringOrderDayToggled event,
Emitter<RecurringOrderState> emit, Emitter<RecurringOrderState> emit,
) { ) {
final List<int> days = List<int>.from(state.recurringDays); final List<String> days = List<String>.from(state.recurringDays);
if (days.contains(event.dayIndex)) { final String label = _dayLabels[event.dayIndex];
days.remove(event.dayIndex); int? autoIndex = state.autoSelectedDayIndex;
if (days.contains(label)) {
days.remove(label);
if (autoIndex == event.dayIndex) {
autoIndex = null;
}
} else { } else {
days.add(event.dayIndex); days.add(label);
days.sort();
} }
emit(state.copyWith(recurringDays: days)); emit(state.copyWith(recurringDays: _sortDays(days), autoSelectedDayIndex: autoIndex));
} }
void _onPositionAdded( void _onPositionAdded(
@@ -277,13 +300,10 @@ class RecurringOrderBloc extends Bloc<RecurringOrderEvent, RecurringOrderState>
if (selectedHub == null) { if (selectedHub == null) {
throw domain.OrderMissingHubException(); throw domain.OrderMissingHubException();
} }
final List<String> recurringDays = state.recurringDays
.map((int index) => _dayLabels[index])
.toList();
final domain.RecurringOrder order = domain.RecurringOrder( final domain.RecurringOrder order = domain.RecurringOrder(
startDate: state.startDate, startDate: state.startDate,
endDate: state.endDate, endDate: state.endDate,
recurringDays: recurringDays, recurringDays: state.recurringDays,
location: selectedHub.name, location: selectedHub.name,
positions: state.positions positions: state.positions
.map( .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.location,
required this.eventName, required this.eventName,
required this.positions, required this.positions,
required this.autoSelectedDayIndex,
this.status = RecurringOrderStatus.initial, this.status = RecurringOrderStatus.initial,
this.errorMessage, this.errorMessage,
this.vendors = const <Vendor>[], this.vendors = const <Vendor>[],
@@ -23,15 +24,26 @@ class RecurringOrderState extends Equatable {
factory RecurringOrderState.initial() { factory RecurringOrderState.initial() {
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
final DateTime start = DateTime(now.year, now.month, now.day); 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( return RecurringOrderState(
startDate: start, startDate: start,
endDate: start.add(const Duration(days: 7)), endDate: start.add(const Duration(days: 7)),
recurringDays: const <int>[], recurringDays: <String>[dayLabels[weekdayIndex]],
location: '', location: '',
eventName: '', eventName: '',
positions: const <RecurringOrderPosition>[ positions: const <RecurringOrderPosition>[
RecurringOrderPosition(role: '', count: 1, startTime: '', endTime: ''), RecurringOrderPosition(role: '', count: 1, startTime: '', endTime: ''),
], ],
autoSelectedDayIndex: weekdayIndex,
vendors: const <Vendor>[], vendors: const <Vendor>[],
hubs: const <RecurringOrderHubOption>[], hubs: const <RecurringOrderHubOption>[],
roles: const <RecurringOrderRoleOption>[], roles: const <RecurringOrderRoleOption>[],
@@ -40,10 +52,11 @@ class RecurringOrderState extends Equatable {
final DateTime startDate; final DateTime startDate;
final DateTime endDate; final DateTime endDate;
final List<int> recurringDays; final List<String> recurringDays;
final String location; final String location;
final String eventName; final String eventName;
final List<RecurringOrderPosition> positions; final List<RecurringOrderPosition> positions;
final int? autoSelectedDayIndex;
final RecurringOrderStatus status; final RecurringOrderStatus status;
final String? errorMessage; final String? errorMessage;
final List<Vendor> vendors; final List<Vendor> vendors;
@@ -55,10 +68,11 @@ class RecurringOrderState extends Equatable {
RecurringOrderState copyWith({ RecurringOrderState copyWith({
DateTime? startDate, DateTime? startDate,
DateTime? endDate, DateTime? endDate,
List<int>? recurringDays, List<String>? recurringDays,
String? location, String? location,
String? eventName, String? eventName,
List<RecurringOrderPosition>? positions, List<RecurringOrderPosition>? positions,
int? autoSelectedDayIndex,
RecurringOrderStatus? status, RecurringOrderStatus? status,
String? errorMessage, String? errorMessage,
List<Vendor>? vendors, List<Vendor>? vendors,
@@ -74,6 +88,7 @@ class RecurringOrderState extends Equatable {
location: location ?? this.location, location: location ?? this.location,
eventName: eventName ?? this.eventName, eventName: eventName ?? this.eventName,
positions: positions ?? this.positions, positions: positions ?? this.positions,
autoSelectedDayIndex: autoSelectedDayIndex ?? this.autoSelectedDayIndex,
status: status ?? this.status, status: status ?? this.status,
errorMessage: errorMessage ?? this.errorMessage, errorMessage: errorMessage ?? this.errorMessage,
vendors: vendors ?? this.vendors, vendors: vendors ?? this.vendors,
@@ -109,6 +124,7 @@ class RecurringOrderState extends Equatable {
location, location,
eventName, eventName,
positions, positions,
autoSelectedDayIndex,
status, status,
errorMessage, errorMessage,
vendors, vendors,

View File

@@ -324,16 +324,33 @@ class _RecurringDaysSelector extends StatelessWidget {
required this.onToggle, required this.onToggle,
}); });
final List<int> selectedDays; final List<String> selectedDays;
final ValueChanged<int> onToggle; final ValueChanged<int> onToggle;
@override @override
Widget build(BuildContext context) { 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( return Wrap(
spacing: UiConstants.space2, spacing: UiConstants.space2,
children: List<Widget>.generate(labels.length, (int index) { children: List<Widget>.generate(labelsShort.length, (int index) {
final bool isSelected = selectedDays.contains(index); final bool isSelected = selectedDays.contains(labelsLong[index]);
return GestureDetector( return GestureDetector(
onTap: () => onToggle(index), onTap: () => onToggle(index),
child: Container( child: Container(
@@ -346,7 +363,7 @@ class _RecurringDaysSelector extends StatelessWidget {
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
labels[index], labelsShort[index],
style: UiTypography.body2m.copyWith( style: UiTypography.body2m.copyWith(
color: isSelected ? UiColors.white : UiColors.textSecondary, color: isSelected ? UiColors.white : UiColors.textSecondary,
), ),