feat: Introduce ShiftScheduleSummarySection to display shift type, date range, and recurring days on the shift details page.
This commit is contained in:
@@ -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 &&
|
||||
|
||||
@@ -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,31 +72,10 @@ 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),
|
||||
_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),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,8 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
orderId: first.orderId,
|
||||
orderType: first.orderType,
|
||||
schedules: schedules,
|
||||
recurringDays: first.recurringDays,
|
||||
permanentDays: first.permanentDays,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user