recurring v2
This commit is contained in:
@@ -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':
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user