feat: Introduce ShiftScheduleSummarySection to display shift type, date range, and recurring days on the shift details page.

This commit is contained in:
Achintha Isuru
2026-02-22 16:48:05 -05:00
parent 6e81d403c3
commit c48d981ddb
5 changed files with 180 additions and 39 deletions

View File

@@ -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<ShiftDetailsPage> {
}
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<ShiftDetailsPage> {
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<ShiftDetailsPage> {
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 &&

View File

@@ -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<String>? recurringDays;
final List<String>? 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,
),
],
),

View File

@@ -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<String> weekDays = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
final Set<int> 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<int> _getActiveWeekdayIndices() {
final List<String> 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();
}
}

View File

@@ -128,6 +128,8 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
orderId: first.orderId,
orderType: first.orderType,
schedules: schedules,
recurringDays: first.recurringDays,
permanentDays: first.permanentDays,
),
);
}

View File

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