feat: Enhance order details page with date range and clock-in/out labels, and improve OrderScheduleSection layout

This commit is contained in:
Achintha Isuru
2026-03-19 15:40:26 -04:00
parent e1b9ad532b
commit fea679b84c
6 changed files with 179 additions and 54 deletions

View File

@@ -1885,6 +1885,7 @@
"spots_left": "${count} spot(s) left",
"shifts_count": "${count} shift(s)",
"schedule_label": "SCHEDULE",
"date_range_label": "Date Range",
"booking_success": "Order booked successfully!",
"booking_pending": "Your booking is pending approval",
"booking_confirmed": "Your booking has been confirmed!",

View File

@@ -1885,6 +1885,7 @@
"spots_left": "${count} puesto(s) disponible(s)",
"shifts_count": "${count} turno(s)",
"schedule_label": "HORARIO",
"date_range_label": "Rango de Fechas",
"booking_success": "\u00a1Orden reservada con \u00e9xito!",
"booking_pending": "Tu reserva est\u00e1 pendiente de aprobaci\u00f3n",
"booking_confirmed": "\u00a1Tu reserva ha sido confirmada!",

View File

@@ -139,6 +139,12 @@ class _OrderDetailsPageState extends State<OrderDetailsPage> {
schedule: order.schedule,
scheduleLabel:
context.t.available_orders.schedule_label,
dateRangeLabel:
context.t.available_orders.date_range_label,
clockInLabel:
context.t.staff_shifts.shift_details.start_time,
clockOutLabel:
context.t.staff_shifts.shift_details.end_time,
shiftsCountLabel: t.available_orders.shifts_count(
count: order.schedule.totalShifts,
),

View File

@@ -79,7 +79,7 @@ class OrderDetailsBottomBar extends StatelessWidget {
width: double.infinity,
child: UiButton.primary(
onPressed: onBook,
text: t.available_orders.book_order,
text: 'Book Shift',
),
);
}

View File

@@ -5,13 +5,18 @@ import 'package:krow_domain/krow_domain.dart';
/// A section displaying the schedule for an available order.
///
/// Shows the days-of-week chips, date range, time range, and total shift count.
/// Shows a date range, Google Calendar-style day-of-week circles,
/// clock-in/clock-out time boxes, and total shift count.
/// Follows the same visual structure as [ShiftDateTimeSection].
class OrderScheduleSection extends StatelessWidget {
/// Creates an [OrderScheduleSection].
const OrderScheduleSection({
super.key,
required this.schedule,
required this.scheduleLabel,
required this.dateRangeLabel,
required this.clockInLabel,
required this.clockOutLabel,
required this.shiftsCountLabel,
});
@@ -21,9 +26,40 @@ class OrderScheduleSection extends StatelessWidget {
/// Localised section title (e.g. "SCHEDULE").
final String scheduleLabel;
/// Localised label for the date range row (e.g. "Date Range").
final String dateRangeLabel;
/// Localised label for the clock-in time box (e.g. "START TIME").
final String clockInLabel;
/// Localised label for the clock-out time box (e.g. "END TIME").
final String clockOutLabel;
/// Localised shifts count text (e.g. "3 shift(s)").
final String shiftsCountLabel;
/// All seven days in ISO order for the day-of-week row.
static const List<DayOfWeek> _allDays = <DayOfWeek>[
DayOfWeek.mon,
DayOfWeek.tue,
DayOfWeek.wed,
DayOfWeek.thu,
DayOfWeek.fri,
DayOfWeek.sat,
DayOfWeek.sun,
];
/// Single-letter labels for each day (ISO order).
static const List<String> _dayLabels = <String>[
'M',
'T',
'W',
'T',
'F',
'S',
'S',
];
/// Formats a date-only string (e.g. "2026-03-24") to "Mar 24".
String _formatDateShort(String dateStr) {
if (dateStr.isEmpty) return '';
@@ -35,96 +71,149 @@ class OrderScheduleSection extends StatelessWidget {
}
}
/// Formats a DateTime to a time string (e.g. "9:00am").
String _formatTime(DateTime dt) {
return DateFormat('h:mma').format(dt).toLowerCase();
/// Formats [DateTime] to a time string (e.g. "9:00 AM").
String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt);
/// Builds the date range display string including the year.
String _buildDateRangeText() {
final String start = _formatDateShort(schedule.startDate);
final String end = _formatDateShort(schedule.endDate);
// Extract year from endDate for display.
String year = '';
if (schedule.endDate.isNotEmpty) {
try {
final DateTime endDt = DateTime.parse(schedule.endDate);
year = ', ${endDt.year}';
} catch (_) {
// Ignore parse errors.
}
}
return '$start - $end$year';
}
@override
Widget build(BuildContext context) {
final String dateRange =
'${_formatDateShort(schedule.startDate)} - ${_formatDateShort(schedule.endDate)}';
final String timeRange =
'${_formatTime(schedule.firstShiftStartsAt)} - ${_formatTime(schedule.lastShiftEndsAt)}';
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space6,
vertical: UiConstants.space4,
),
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Section title
Text(
scheduleLabel,
style: UiTypography.titleUppercase4b.textSecondary,
),
const SizedBox(height: UiConstants.space3),
// Days of week chips
if (schedule.daysOfWeek.isNotEmpty) ...<Widget>[
Wrap(
spacing: UiConstants.space1,
runSpacing: UiConstants.space1,
children: schedule.daysOfWeek
.map((DayOfWeek day) => _buildDayChip(day))
.toList(),
),
const SizedBox(height: UiConstants.space3),
],
const SizedBox(height: UiConstants.space4),
// Date range row
Row(
children: <Widget>[
const Icon(
UiIcons.calendar,
size: 20,
color: UiColors.primary,
size: UiConstants.space5,
color: UiColors.textPrimary,
),
const SizedBox(width: UiConstants.space2),
Text(dateRange, style: UiTypography.headline5m.textPrimary),
Text(
_buildDateRangeText(),
style: UiTypography.title1m.textPrimary,
),
],
),
const SizedBox(height: UiConstants.space2),
const SizedBox(height: UiConstants.space6),
// Time range row
// Days-of-week circles (Google Calendar style)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
for (int i = 0; i < _allDays.length; i++)
_buildDayCircle(
_allDays[i],
_dayLabels[i],
schedule.daysOfWeek.contains(_allDays[i]),
),
],
),
const SizedBox(height: UiConstants.space6),
// Clock in / Clock out time boxes
Row(
children: <Widget>[
const Icon(
UiIcons.clock,
size: 20,
color: UiColors.primary,
Expanded(
child: _buildTimeBox(clockInLabel, schedule.firstShiftStartsAt),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: _buildTimeBox(clockOutLabel, schedule.lastShiftEndsAt),
),
const SizedBox(width: UiConstants.space2),
Text(timeRange, style: UiTypography.headline5m.textPrimary),
],
),
const SizedBox(height: UiConstants.space2),
const SizedBox(height: UiConstants.space8),
Text(
'TOTAL SHIFTS',
style: UiTypography.titleUppercase4b.textSecondary,
),
const SizedBox(height: UiConstants.space2),
// Shifts count
Text(shiftsCountLabel, style: UiTypography.footnote2r.textSecondary),
Text(shiftsCountLabel, style: UiTypography.body1r),
],
),
);
}
/// Builds a small chip showing a day-of-week abbreviation.
Widget _buildDayChip(DayOfWeek day) {
final String label = day.value.isNotEmpty
? '${day.value[0]}${day.value.substring(1).toLowerCase()}'
: '';
/// Builds a single day-of-week circle.
///
/// Active days are filled with the primary color and white text.
/// Inactive days use the background color and secondary text.
Widget _buildDayCircle(DayOfWeek day, String label, bool isActive) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: 2,
),
width: 32,
height: 32,
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.08),
borderRadius: UiConstants.radiusSm,
border: Border.all(
color: isActive ? UiColors.primary : UiColors.background,
width: 1.5,
),
color: isActive ? UiColors.primary.withAlpha(40) : UiColors.background,
shape: BoxShape.circle,
),
child: Text(
label,
style: UiTypography.footnote2m.copyWith(color: UiColors.primary),
child: Center(
child: Text(
label,
style: isActive
? UiTypography.footnote1b.primary
: UiTypography.footnote2m.textSecondary,
),
),
);
}
/// Builds a time-display box matching the [ShiftDateTimeSection] pattern.
Widget _buildTimeBox(String label, DateTime time) {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.bgThird,
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Column(
children: <Widget>[
Text(
label,
style: UiTypography.footnote2b.copyWith(
color: UiColors.textSecondary,
letterSpacing: 0.5,
),
),
const SizedBox(height: UiConstants.space1),
Text(
_formatTime(time),
style: UiTypography.title1m
.copyWith(fontWeight: FontWeight.w700)
.textPrimary,
),
],
),
);
}