From 6e81d403c349a7ff28b74ba133c27a45ce968241 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 22 Feb 2026 15:15:41 -0500 Subject: [PATCH] feat: Add support for displaying recurring shift details including start/end dates and recurring days. --- .../shifts_connector_repository_impl.dart | 102 +++++++++++++++++- .../domain/lib/src/entities/shifts/shift.dart | 12 +++ .../pages/shift_details_page.dart | 9 +- .../presentation/widgets/my_shift_card.dart | 26 ++--- .../shift_date_time_section.dart | 66 +++++++----- .../connector/shiftRole/queries.gql | 2 + 6 files changed, 172 insertions(+), 45 deletions(-) diff --git a/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart b/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart index d6f7c2ed..c09de9c3 100644 --- a/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart +++ b/apps/mobile/packages/data_connect/lib/src/connectors/shifts/data/repositories/shifts_connector_repository_impl.dart @@ -87,6 +87,27 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository { final String orderTypeStr = sr.shift.order.orderType.stringValue .toUpperCase(); + final Map 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? schedules = _generateSchedules( + orderType: orderTypeStr, + startDate: startDate, + endDate: endDate, + recurringDays: sr.shift.order.recurringDays, + permanentDays: sr.shift.order.permanentDays, + startTime: startTime, + endTime: endTime, + ); + mappedShifts.add( Shift( id: sr.shiftId, @@ -98,14 +119,12 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository { location: sr.shift.location ?? '', locationAddress: sr.shift.locationAddress ?? '', date: shiftDate?.toIso8601String() ?? '', - startTime: startDt != null - ? DateFormat('HH:mm').format(startDt) - : '', - endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '', + startTime: startTime, + endTime: endTime, createdDate: createdDt?.toIso8601String() ?? '', status: sr.shift.status?.stringValue.toLowerCase() ?? 'open', description: sr.shift.description, - durationDays: sr.shift.durationDays, + durationDays: sr.shift.durationDays ?? schedules?.length, requiredSlots: sr.count, filledSlots: sr.assigned ?? 0, latitude: sr.shift.latitude, @@ -114,6 +133,11 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository { // FindShiftsTab._groupMultiDayShifts and MyShiftCard._getShiftType. orderId: sr.shift.orderId, orderType: orderTypeStr, + startDate: startDate?.toIso8601String(), + endDate: endDate?.toIso8601String(), + recurringDays: sr.shift.order.recurringDays, + permanentDays: sr.shift.order.permanentDays, + schedules: schedules, breakInfo: BreakAdapter.fromData( isPaid: sr.isBreakPaid ?? false, breakTime: sr.breakType?.stringValue, @@ -611,4 +635,72 @@ class ShiftsConnectorRepositoryImpl implements ShiftsConnectorRepository { } }); } + + /// Generates a list of [ShiftSchedule] for RECURRING or PERMANENT orders. + List? _generateSchedules({ + required String orderType, + required DateTime? startDate, + required DateTime? endDate, + required List? recurringDays, + required List? permanentDays, + required String startTime, + required String endTime, + }) { + if (orderType != 'RECURRING' && orderType != 'PERMANENT') return null; + if (startDate == null || endDate == null) return null; + + final List? daysToInclude = orderType == 'RECURRING' + ? recurringDays + : permanentDays; + if (daysToInclude == null || daysToInclude.isEmpty) return null; + + final List schedules = []; + final Set 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; + } } diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart index 92fec9a0..a6d6fdeb 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/shift.dart @@ -34,6 +34,10 @@ class Shift extends Equatable { this.breakInfo, this.orderId, this.orderType, + this.startDate, + this.endDate, + this.recurringDays, + this.permanentDays, this.schedules, }); @@ -68,6 +72,10 @@ class Shift extends Equatable { final Break? breakInfo; final String? orderId; final String? orderType; + final String? startDate; + final String? endDate; + final List? recurringDays; + final List? permanentDays; final List? schedules; @override @@ -103,6 +111,10 @@ class Shift extends Equatable { breakInfo, orderId, orderType, + startDate, + endDate, + recurringDays, + permanentDays, schedules, ]; } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index b2a17a60..367553e5 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -116,8 +116,9 @@ class _ShiftDetailsPageState extends State { ); } - final Shift displayShift = - state is ShiftDetailsLoaded ? state.shift : widget.shift; + final Shift displayShift = state is ShiftDetailsLoaded + ? state.shift + : widget.shift; final i18n = Translations.of(context).staff_shifts.shift_details; final duration = _calculateDuration(displayShift); @@ -154,6 +155,10 @@ class _ShiftDetailsPageState extends State { shiftDateLabel: i18n.shift_date, clockInLabel: i18n.start_time, clockOutLabel: i18n.end_time, + startDate: displayShift.startDate, + endDate: displayShift.endDate, + recurringDays: displayShift.recurringDays, + permanentDays: displayShift.permanentDays, ), const Divider(height: 1, thickness: 0.5), if (displayShift.breakInfo != null && diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart index 37cafb9c..36f59053 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -342,31 +342,31 @@ class _MyShiftCardState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ const Icon( UiIcons.clock, size: UiConstants.iconXs, - color: UiColors.primary, + color: UiColors.iconSecondary, ), const SizedBox(width: UiConstants.space1), Text( - '${schedules.length} schedules', - style: UiTypography.footnote2m.copyWith( - color: UiColors.primary, - ), + scheduleRange, + style: + UiTypography.footnote2r.textSecondary, ), ], ), - const SizedBox(height: UiConstants.space1), - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: Text( - scheduleRange, - style: UiTypography.footnote2r.copyWith( - color: UiColors.primary, - ), + + const SizedBox(height: UiConstants.space2), + + Text( + '${schedules.length} schedules', + style: UiTypography.footnote2m.copyWith( + color: UiColors.primary, ), ), + const SizedBox(height: UiConstants.space1), ...visibleSchedules.map( (schedule) => Padding( padding: const EdgeInsets.only(bottom: 2), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart index 47eded2f..251ed6cd 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart @@ -6,19 +6,19 @@ import 'package:intl/intl.dart'; class ShiftDateTimeSection extends StatelessWidget { /// The ISO string of the date. final String date; - + /// The start time string (HH:mm). final String startTime; - + /// The end time string (HH:mm). final String endTime; - + /// Localization string for shift date. final String shiftDateLabel; - + /// Localization string for clock in time. final String clockInLabel; - + /// Localization string for clock out time. final String clockOutLabel; @@ -31,8 +31,17 @@ class ShiftDateTimeSection extends StatelessWidget { required this.shiftDateLabel, required this.clockInLabel, required this.clockOutLabel, + this.startDate, + this.endDate, + this.recurringDays, + this.permanentDays, }); + final String? startDate; + final String? endDate; + final List? recurringDays; + final List? permanentDays; + String _formatTime(String time) { if (time.isEmpty) return ''; try { @@ -70,34 +79,41 @@ class ShiftDateTimeSection extends StatelessWidget { const SizedBox(height: UiConstants.space2), Row( children: [ - const Icon( - UiIcons.calendar, - size: 20, - color: UiColors.primary, - ), + const Icon(UiIcons.calendar, size: 20, color: UiColors.primary), const SizedBox(width: UiConstants.space2), - Text( - _formatDate(date), - style: UiTypography.headline5m.textPrimary, + Expanded( + child: Column( + 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), Row( children: [ - Expanded( - child: _buildTimeBox( - clockInLabel, - startTime, - ), - ), + Expanded(child: _buildTimeBox(clockInLabel, startTime)), const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildTimeBox( - clockOutLabel, - endTime, - ), - ), + Expanded(child: _buildTimeBox(clockOutLabel, endTime)), ], ), ], diff --git a/backend/dataconnect/connector/shiftRole/queries.gql b/backend/dataconnect/connector/shiftRole/queries.gql index 664739c9..7b525502 100644 --- a/backend/dataconnect/connector/shiftRole/queries.gql +++ b/backend/dataconnect/connector/shiftRole/queries.gql @@ -306,6 +306,8 @@ query listShiftRolesByVendorId( orderType status date + startDate + endDate recurringDays permanentDays notes