feat: Add support for displaying recurring shift details including start/end dates and recurring days.
This commit is contained in:
@@ -87,6 +87,27 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
final String orderTypeStr = sr.shift.order.orderType.stringValue
|
final String orderTypeStr = sr.shift.order.orderType.stringValue
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
|
|
||||||
|
final Map<String, dynamic> orderJson = sr.shift.order.toJson();
|
||||||
|
final DateTime? startDate = _service.toDateTime(orderJson['startDate']);
|
||||||
|
final DateTime? endDate = _service.toDateTime(orderJson['endDate']);
|
||||||
|
|
||||||
|
final String startTime = startDt != null
|
||||||
|
? DateFormat('HH:mm').format(startDt)
|
||||||
|
: '';
|
||||||
|
final String endTime = endDt != null
|
||||||
|
? DateFormat('HH:mm').format(endDt)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
final List<ShiftSchedule>? schedules = _generateSchedules(
|
||||||
|
orderType: orderTypeStr,
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
recurringDays: sr.shift.order.recurringDays,
|
||||||
|
permanentDays: sr.shift.order.permanentDays,
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
);
|
||||||
|
|
||||||
mappedShifts.add(
|
mappedShifts.add(
|
||||||
Shift(
|
Shift(
|
||||||
id: sr.shiftId,
|
id: sr.shiftId,
|
||||||
@@ -98,14 +119,12 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
location: sr.shift.location ?? '',
|
location: sr.shift.location ?? '',
|
||||||
locationAddress: sr.shift.locationAddress ?? '',
|
locationAddress: sr.shift.locationAddress ?? '',
|
||||||
date: shiftDate?.toIso8601String() ?? '',
|
date: shiftDate?.toIso8601String() ?? '',
|
||||||
startTime: startDt != null
|
startTime: startTime,
|
||||||
? DateFormat('HH:mm').format(startDt)
|
endTime: endTime,
|
||||||
: '',
|
|
||||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
|
||||||
createdDate: createdDt?.toIso8601String() ?? '',
|
createdDate: createdDt?.toIso8601String() ?? '',
|
||||||
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
||||||
description: sr.shift.description,
|
description: sr.shift.description,
|
||||||
durationDays: sr.shift.durationDays,
|
durationDays: sr.shift.durationDays ?? schedules?.length,
|
||||||
requiredSlots: sr.count,
|
requiredSlots: sr.count,
|
||||||
filledSlots: sr.assigned ?? 0,
|
filledSlots: sr.assigned ?? 0,
|
||||||
latitude: sr.shift.latitude,
|
latitude: sr.shift.latitude,
|
||||||
@@ -114,6 +133,11 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
// FindShiftsTab._groupMultiDayShifts and MyShiftCard._getShiftType.
|
// FindShiftsTab._groupMultiDayShifts and MyShiftCard._getShiftType.
|
||||||
orderId: sr.shift.orderId,
|
orderId: sr.shift.orderId,
|
||||||
orderType: orderTypeStr,
|
orderType: orderTypeStr,
|
||||||
|
startDate: startDate?.toIso8601String(),
|
||||||
|
endDate: endDate?.toIso8601String(),
|
||||||
|
recurringDays: sr.shift.order.recurringDays,
|
||||||
|
permanentDays: sr.shift.order.permanentDays,
|
||||||
|
schedules: schedules,
|
||||||
breakInfo: BreakAdapter.fromData(
|
breakInfo: BreakAdapter.fromData(
|
||||||
isPaid: sr.isBreakPaid ?? false,
|
isPaid: sr.isBreakPaid ?? false,
|
||||||
breakTime: sr.breakType?.stringValue,
|
breakTime: sr.breakType?.stringValue,
|
||||||
@@ -611,4 +635,72 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generates a list of [ShiftSchedule] for RECURRING or PERMANENT orders.
|
||||||
|
List<ShiftSchedule>? _generateSchedules({
|
||||||
|
required String orderType,
|
||||||
|
required DateTime? startDate,
|
||||||
|
required DateTime? endDate,
|
||||||
|
required List<String>? recurringDays,
|
||||||
|
required List<String>? permanentDays,
|
||||||
|
required String startTime,
|
||||||
|
required String endTime,
|
||||||
|
}) {
|
||||||
|
if (orderType != 'RECURRING' && orderType != 'PERMANENT') return null;
|
||||||
|
if (startDate == null || endDate == null) return null;
|
||||||
|
|
||||||
|
final List<String>? daysToInclude = orderType == 'RECURRING'
|
||||||
|
? recurringDays
|
||||||
|
: permanentDays;
|
||||||
|
if (daysToInclude == null || daysToInclude.isEmpty) return null;
|
||||||
|
|
||||||
|
final List<ShiftSchedule> schedules = <ShiftSchedule>[];
|
||||||
|
final Set<int> targetWeekdayIndex = daysToInclude
|
||||||
|
.map((String day) {
|
||||||
|
switch (day.toUpperCase()) {
|
||||||
|
case 'MONDAY':
|
||||||
|
return DateTime.monday;
|
||||||
|
case 'TUESDAY':
|
||||||
|
return DateTime.tuesday;
|
||||||
|
case 'WEDNESDAY':
|
||||||
|
return DateTime.wednesday;
|
||||||
|
case 'THURSDAY':
|
||||||
|
return DateTime.thursday;
|
||||||
|
case 'FRIDAY':
|
||||||
|
return DateTime.friday;
|
||||||
|
case 'SATURDAY':
|
||||||
|
return DateTime.saturday;
|
||||||
|
case 'SUNDAY':
|
||||||
|
return DateTime.sunday;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.where((int idx) => idx != -1)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
DateTime current = startDate;
|
||||||
|
while (current.isBefore(endDate) ||
|
||||||
|
current.isAtSameMomentAs(endDate) ||
|
||||||
|
// Handle cases where the time component might differ slightly by checking date equality
|
||||||
|
(current.year == endDate.year &&
|
||||||
|
current.month == endDate.month &&
|
||||||
|
current.day == endDate.day)) {
|
||||||
|
if (targetWeekdayIndex.contains(current.weekday)) {
|
||||||
|
schedules.add(
|
||||||
|
ShiftSchedule(
|
||||||
|
date: current.toIso8601String(),
|
||||||
|
startTime: startTime,
|
||||||
|
endTime: endTime,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
current = current.add(const Duration(days: 1));
|
||||||
|
|
||||||
|
// Safety break to prevent infinite loops if dates are messed up
|
||||||
|
if (schedules.length > 365) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return schedules;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ class Shift extends Equatable {
|
|||||||
this.breakInfo,
|
this.breakInfo,
|
||||||
this.orderId,
|
this.orderId,
|
||||||
this.orderType,
|
this.orderType,
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
this.recurringDays,
|
||||||
|
this.permanentDays,
|
||||||
this.schedules,
|
this.schedules,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,6 +72,10 @@ class Shift extends Equatable {
|
|||||||
final Break? breakInfo;
|
final Break? breakInfo;
|
||||||
final String? orderId;
|
final String? orderId;
|
||||||
final String? orderType;
|
final String? orderType;
|
||||||
|
final String? startDate;
|
||||||
|
final String? endDate;
|
||||||
|
final List<String>? recurringDays;
|
||||||
|
final List<String>? permanentDays;
|
||||||
final List<ShiftSchedule>? schedules;
|
final List<ShiftSchedule>? schedules;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -103,6 +111,10 @@ class Shift extends Equatable {
|
|||||||
breakInfo,
|
breakInfo,
|
||||||
orderId,
|
orderId,
|
||||||
orderType,
|
orderType,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
recurringDays,
|
||||||
|
permanentDays,
|
||||||
schedules,
|
schedules,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,8 +116,9 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Shift displayShift =
|
final Shift displayShift = state is ShiftDetailsLoaded
|
||||||
state is ShiftDetailsLoaded ? state.shift : widget.shift;
|
? state.shift
|
||||||
|
: widget.shift;
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||||
|
|
||||||
final duration = _calculateDuration(displayShift);
|
final duration = _calculateDuration(displayShift);
|
||||||
@@ -154,6 +155,10 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
shiftDateLabel: i18n.shift_date,
|
shiftDateLabel: i18n.shift_date,
|
||||||
clockInLabel: i18n.start_time,
|
clockInLabel: i18n.start_time,
|
||||||
clockOutLabel: i18n.end_time,
|
clockOutLabel: i18n.end_time,
|
||||||
|
startDate: displayShift.startDate,
|
||||||
|
endDate: displayShift.endDate,
|
||||||
|
recurringDays: displayShift.recurringDays,
|
||||||
|
permanentDays: displayShift.permanentDays,
|
||||||
),
|
),
|
||||||
const Divider(height: 1, thickness: 0.5),
|
const Divider(height: 1, thickness: 0.5),
|
||||||
if (displayShift.breakInfo != null &&
|
if (displayShift.breakInfo != null &&
|
||||||
|
|||||||
@@ -342,31 +342,31 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
UiIcons.clock,
|
UiIcons.clock,
|
||||||
size: UiConstants.iconXs,
|
size: UiConstants.iconXs,
|
||||||
color: UiColors.primary,
|
color: UiColors.iconSecondary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: UiConstants.space1),
|
const SizedBox(width: UiConstants.space1),
|
||||||
Text(
|
Text(
|
||||||
'${schedules.length} schedules',
|
scheduleRange,
|
||||||
style: UiTypography.footnote2m.copyWith(
|
style:
|
||||||
color: UiColors.primary,
|
UiTypography.footnote2r.textSecondary,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space1),
|
|
||||||
Padding(
|
const SizedBox(height: UiConstants.space2),
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
|
||||||
child: Text(
|
Text(
|
||||||
scheduleRange,
|
'${schedules.length} schedules',
|
||||||
style: UiTypography.footnote2r.copyWith(
|
style: UiTypography.footnote2m.copyWith(
|
||||||
color: UiColors.primary,
|
color: UiColors.primary,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: UiConstants.space1),
|
||||||
...visibleSchedules.map(
|
...visibleSchedules.map(
|
||||||
(schedule) => Padding(
|
(schedule) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 2),
|
padding: const EdgeInsets.only(bottom: 2),
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ import 'package:intl/intl.dart';
|
|||||||
class ShiftDateTimeSection extends StatelessWidget {
|
class ShiftDateTimeSection extends StatelessWidget {
|
||||||
/// The ISO string of the date.
|
/// The ISO string of the date.
|
||||||
final String date;
|
final String date;
|
||||||
|
|
||||||
/// The start time string (HH:mm).
|
/// The start time string (HH:mm).
|
||||||
final String startTime;
|
final String startTime;
|
||||||
|
|
||||||
/// The end time string (HH:mm).
|
/// The end time string (HH:mm).
|
||||||
final String endTime;
|
final String endTime;
|
||||||
|
|
||||||
/// Localization string for shift date.
|
/// Localization string for shift date.
|
||||||
final String shiftDateLabel;
|
final String shiftDateLabel;
|
||||||
|
|
||||||
/// Localization string for clock in time.
|
/// Localization string for clock in time.
|
||||||
final String clockInLabel;
|
final String clockInLabel;
|
||||||
|
|
||||||
/// Localization string for clock out time.
|
/// Localization string for clock out time.
|
||||||
final String clockOutLabel;
|
final String clockOutLabel;
|
||||||
|
|
||||||
@@ -31,8 +31,17 @@ class ShiftDateTimeSection extends StatelessWidget {
|
|||||||
required this.shiftDateLabel,
|
required this.shiftDateLabel,
|
||||||
required this.clockInLabel,
|
required this.clockInLabel,
|
||||||
required this.clockOutLabel,
|
required this.clockOutLabel,
|
||||||
|
this.startDate,
|
||||||
|
this.endDate,
|
||||||
|
this.recurringDays,
|
||||||
|
this.permanentDays,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String? startDate;
|
||||||
|
final String? endDate;
|
||||||
|
final List<String>? recurringDays;
|
||||||
|
final List<String>? permanentDays;
|
||||||
|
|
||||||
String _formatTime(String time) {
|
String _formatTime(String time) {
|
||||||
if (time.isEmpty) return '';
|
if (time.isEmpty) return '';
|
||||||
try {
|
try {
|
||||||
@@ -70,34 +79,41 @@ class ShiftDateTimeSection extends StatelessWidget {
|
|||||||
const SizedBox(height: UiConstants.space2),
|
const SizedBox(height: UiConstants.space2),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(UiIcons.calendar, size: 20, color: UiColors.primary),
|
||||||
UiIcons.calendar,
|
|
||||||
size: 20,
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
Text(
|
Expanded(
|
||||||
_formatDate(date),
|
child: Column(
|
||||||
style: UiTypography.headline5m.textPrimary,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
startDate != null && endDate != null
|
||||||
|
? '${DateFormat('MMM d, y').format(DateTime.parse(startDate!))} – ${DateFormat('MMM d, y').format(DateTime.parse(endDate!))}'
|
||||||
|
: _formatDate(date),
|
||||||
|
style: UiTypography.headline5m.textPrimary,
|
||||||
|
),
|
||||||
|
if (recurringDays != null || permanentDays != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
(recurringDays ?? permanentDays ?? [])
|
||||||
|
.map(
|
||||||
|
(d) =>
|
||||||
|
'${d[0].toUpperCase()}${d.substring(1, 3).toLowerCase()}',
|
||||||
|
)
|
||||||
|
.join(', '),
|
||||||
|
style: UiTypography.footnote2r.textSecondary,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: UiConstants.space4),
|
const SizedBox(height: UiConstants.space4),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(child: _buildTimeBox(clockInLabel, startTime)),
|
||||||
child: _buildTimeBox(
|
|
||||||
clockInLabel,
|
|
||||||
startTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space4),
|
const SizedBox(width: UiConstants.space4),
|
||||||
Expanded(
|
Expanded(child: _buildTimeBox(clockOutLabel, endTime)),
|
||||||
child: _buildTimeBox(
|
|
||||||
clockOutLabel,
|
|
||||||
endTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -306,6 +306,8 @@ query listShiftRolesByVendorId(
|
|||||||
orderType
|
orderType
|
||||||
status
|
status
|
||||||
date
|
date
|
||||||
|
startDate
|
||||||
|
endDate
|
||||||
recurringDays
|
recurringDays
|
||||||
permanentDays
|
permanentDays
|
||||||
notes
|
notes
|
||||||
|
|||||||
Reference in New Issue
Block a user