feat: Add support for displaying recurring shift details including start/end dates and recurring days.

This commit is contained in:
Achintha Isuru
2026-02-22 15:15:41 -05:00
parent 415475acb6
commit 6e81d403c3
6 changed files with 172 additions and 45 deletions

View File

@@ -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;
}
} }

View File

@@ -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,
]; ];
} }

View File

@@ -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 &&

View File

@@ -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),

View File

@@ -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,
),
),
], ],
), ),
], ],

View File

@@ -306,6 +306,8 @@ query listShiftRolesByVendorId(
orderType orderType
status status
date date
startDate
endDate
recurringDays recurringDays
permanentDays permanentDays
notes notes