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_bottom_bar.dart';
|
||||||
import '../widgets/shift_details/shift_details_header.dart';
|
import '../widgets/shift_details/shift_details_header.dart';
|
||||||
import '../widgets/shift_details/shift_location_section.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';
|
import '../widgets/shift_details/shift_stats_row.dart';
|
||||||
|
|
||||||
class ShiftDetailsPage extends StatefulWidget {
|
class ShiftDetailsPage extends StatefulWidget {
|
||||||
@@ -117,7 +118,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final Shift displayShift = state is ShiftDetailsLoaded
|
final Shift displayShift = state is ShiftDetailsLoaded
|
||||||
? state.shift
|
? widget.shift
|
||||||
: widget.shift;
|
: widget.shift;
|
||||||
final i18n = Translations.of(context).staff_shifts.shift_details;
|
final i18n = Translations.of(context).staff_shifts.shift_details;
|
||||||
|
|
||||||
@@ -139,6 +140,8 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
children: [
|
children: [
|
||||||
ShiftDetailsHeader(shift: displayShift),
|
ShiftDetailsHeader(shift: displayShift),
|
||||||
const Divider(height: 1, thickness: 0.5),
|
const Divider(height: 1, thickness: 0.5),
|
||||||
|
ShiftScheduleSummarySection(shift: displayShift),
|
||||||
|
const Divider(height: 1, thickness: 0.5),
|
||||||
ShiftStatsRow(
|
ShiftStatsRow(
|
||||||
estimatedTotal: estimatedTotal,
|
estimatedTotal: estimatedTotal,
|
||||||
hourlyRate: displayShift.hourlyRate,
|
hourlyRate: displayShift.hourlyRate,
|
||||||
@@ -155,10 +158,6 @@ 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 &&
|
||||||
|
|||||||
@@ -31,17 +31,8 @@ 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 {
|
||||||
@@ -81,30 +72,9 @@ class ShiftDateTimeSection extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Icon(UiIcons.calendar, size: 20, color: UiColors.primary),
|
const Icon(UiIcons.calendar, size: 20, color: UiColors.primary),
|
||||||
const SizedBox(width: UiConstants.space2),
|
const SizedBox(width: UiConstants.space2),
|
||||||
Expanded(
|
Text(
|
||||||
child: Column(
|
_formatDate(date),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
style: UiTypography.headline5m.textPrimary,
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
orderId: first.orderId,
|
||||||
orderType: first.orderType,
|
orderType: first.orderType,
|
||||||
schedules: schedules,
|
schedules: schedules,
|
||||||
|
recurringDays: first.recurringDays,
|
||||||
|
permanentDays: first.permanentDays,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ class ShiftDetailsModule extends Module {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
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