From c48d981ddbcb708e9c4e9240d1efa8fd82fd96c6 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sun, 22 Feb 2026 16:48:05 -0500 Subject: [PATCH] feat: Introduce ShiftScheduleSummarySection to display shift type, date range, and recurring days on the shift details page. --- .../pages/shift_details_page.dart | 9 +- .../shift_date_time_section.dart | 36 +--- .../shift_schedule_summary_section.dart | 166 ++++++++++++++++++ .../widgets/tabs/find_shifts_tab.dart | 2 + .../shifts/lib/src/shift_details_module.dart | 6 +- 5 files changed, 180 insertions(+), 39 deletions(-) create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_schedule_summary_section.dart 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 367553e5..117be7a1 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 @@ -16,6 +16,7 @@ import '../widgets/shift_details/shift_description_section.dart'; import '../widgets/shift_details/shift_details_bottom_bar.dart'; import '../widgets/shift_details/shift_details_header.dart'; import '../widgets/shift_details/shift_location_section.dart'; +import '../widgets/shift_details/shift_schedule_summary_section.dart'; import '../widgets/shift_details/shift_stats_row.dart'; class ShiftDetailsPage extends StatefulWidget { @@ -117,7 +118,7 @@ class _ShiftDetailsPageState extends State { } final Shift displayShift = state is ShiftDetailsLoaded - ? state.shift + ? widget.shift : widget.shift; final i18n = Translations.of(context).staff_shifts.shift_details; @@ -139,6 +140,8 @@ class _ShiftDetailsPageState extends State { children: [ ShiftDetailsHeader(shift: displayShift), const Divider(height: 1, thickness: 0.5), + ShiftScheduleSummarySection(shift: displayShift), + const Divider(height: 1, thickness: 0.5), ShiftStatsRow( estimatedTotal: estimatedTotal, hourlyRate: displayShift.hourlyRate, @@ -155,10 +158,6 @@ 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/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 251ed6cd..b4b7c07f 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 @@ -31,17 +31,8 @@ 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 { @@ -81,30 +72,9 @@ class ShiftDateTimeSection extends StatelessWidget { children: [ const Icon(UiIcons.calendar, size: 20, color: UiColors.primary), const SizedBox(width: UiConstants.space2), - 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, - ), - ], - ], - ), + Text( + _formatDate(date), + style: UiTypography.headline5m.textPrimary, ), ], ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_schedule_summary_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_schedule_summary_section.dart new file mode 100644 index 00000000..38d70b04 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_schedule_summary_section.dart @@ -0,0 +1,166 @@ +import 'package:core_localization/core_localization.dart'; +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// A section displaying the shift type, date range, and weekday schedule summary. +class ShiftScheduleSummarySection extends StatelessWidget { + /// The shift entity. + final Shift shift; + + /// Creates a [ShiftScheduleSummarySection]. + const ShiftScheduleSummarySection({super.key, required this.shift}); + + String _getShiftTypeLabel(Translations t) { + final String type = (shift.orderType ?? '').toUpperCase(); + if (type == 'PERMANENT') { + return t.staff_shifts.filter.long_term; + } + if (type == 'RECURRING') { + return t.staff_shifts.filter.multi_day; + } + return t.staff_shifts.filter.one_day; + } + + bool _isMultiDayOrLongTerm() { + final String type = (shift.orderType ?? '').toUpperCase(); + return type == 'RECURRING' || type == 'PERMANENT'; + } + + @override + Widget build(BuildContext context) { + final t = Translations.of(context); + final isMultiDay = _isMultiDayOrLongTerm(); + final typeLabel = _getShiftTypeLabel(t); + final String orderType = (shift.orderType ?? '').toUpperCase(); + + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Shift Type Title + Text( + typeLabel.toUpperCase(), + style: UiTypography.titleUppercase4b.textSecondary, + ), + + if (isMultiDay) ...[ + const SizedBox(height: UiConstants.space3), + + // Date Range + if (shift.startDate != null && shift.endDate != null) + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 16, + color: UiColors.textSecondary, + ), + const SizedBox(width: UiConstants.space2), + Text( + '${_formatDate(shift.startDate!)} – ${_formatDate(shift.endDate!)}', + style: UiTypography.body2m.textPrimary, + ), + ], + ), + + const SizedBox(height: UiConstants.space4), + + // Weekday Circles + _buildWeekdaySchedule(context), + + // Available Shifts Count (Only for RECURRING/Multi-Day) + if (orderType == 'RECURRING' && shift.schedules != null) ...[ + const SizedBox(height: UiConstants.space4), + Row( + children: [ + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: UiColors.success, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: UiConstants.space2), + Text( + '${shift.schedules!.length} available shifts', + style: UiTypography.body2b.copyWith( + color: UiColors.textSuccess, + ), + ), + ], + ), + ], + ], + ], + ), + ); + } + + String _formatDate(String dateStr) { + try { + final date = DateTime.parse(dateStr); + return DateFormat('MMM d, y').format(date); + } catch (_) { + return dateStr; + } + } + + Widget _buildWeekdaySchedule(BuildContext context) { + final List weekDays = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; + final Set activeDays = _getActiveWeekdayIndices(); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: List.generate(weekDays.length, (index) { + final bool isActive = activeDays.contains(index + 1); // 1-7 (Mon-Sun) + return Container( + width: 38, + height: 38, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isActive ? UiColors.primaryInverse : UiColors.bgThird, + border: Border.all( + color: isActive ? UiColors.primary : UiColors.border, + ), + ), + child: Center( + child: Text( + weekDays[index], + style: UiTypography.body2b.copyWith( + color: isActive ? UiColors.primary : UiColors.textSecondary, + ), + ), + ), + ); + }), + ); + } + + Set _getActiveWeekdayIndices() { + final List days = shift.recurringDays ?? shift.permanentDays ?? []; + return days.map((day) { + switch (day.toUpperCase()) { + case 'MON': + return DateTime.monday; + case 'TUE': + return DateTime.tuesday; + case 'WED': + return DateTime.wednesday; + case 'THU': + return DateTime.thursday; + case 'FRI': + return DateTime.friday; + case 'SAT': + return DateTime.saturday; + case 'SUN': + return DateTime.sunday; + default: + return -1; + } + }).toSet(); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index 863b2afe..8e3c6a46 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -128,6 +128,8 @@ class _FindShiftsTabState extends State { orderId: first.orderId, orderType: first.orderType, schedules: schedules, + recurringDays: first.recurringDays, + permanentDays: first.permanentDays, ), ); } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart index 78fddf80..f22fc524 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart @@ -26,6 +26,10 @@ class ShiftDetailsModule extends Module { @override void routes(RouteManager r) { - r.child('/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data)); + r.child( + '/:id', + child: (_) => + ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data), + ); } }