diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/available_order_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/available_order_card.dart index fa64c9af..81bc8cf9 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/available_order_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/available_order_card.dart @@ -6,7 +6,8 @@ import 'package:krow_domain/krow_domain.dart'; /// Card displaying an [AvailableOrder] from the staff marketplace. /// -/// Shows role, location, schedule, pay rate, and a booking/apply action. +/// Shows role, pay (total + hourly), time, date, client, location, +/// schedule chips, and a booking/apply action. class AvailableOrderCard extends StatelessWidget { /// Creates an [AvailableOrderCard]. const AvailableOrderCard({ @@ -25,6 +26,10 @@ class AvailableOrderCard extends StatelessWidget { /// Whether a booking request is currently in progress. final bool bookingInProgress; + String _formatTime(DateTime time) { + return DateFormat('h:mma').format(time).toLowerCase(); + } + /// Formats a date-only string (e.g. "2026-03-24") to "Mar 24". String _formatDateShort(String dateStr) { if (dateStr.isEmpty) return ''; @@ -36,6 +41,16 @@ class AvailableOrderCard extends StatelessWidget { } } + /// Computes the duration in hours from the first shift start to end. + double _durationHours() { + final int minutes = order.schedule.lastShiftEndsAt + .difference(order.schedule.firstShiftStartsAt) + .inMinutes; + double hours = minutes / 60; + if (hours < 0) hours += 24; + return hours.roundToDouble(); + } + /// Returns a human-readable label for the order type. String _orderTypeLabel(OrderType type) { switch (type) { @@ -70,12 +85,12 @@ class AvailableOrderCard extends StatelessWidget { Widget build(BuildContext context) { final AvailableOrderSchedule schedule = order.schedule; final int spotsLeft = order.requiredWorkerCount - order.filledCount; - final String hourlyDisplay = - '\$${order.hourlyRate.toStringAsFixed(order.hourlyRate.truncateToDouble() == order.hourlyRate ? 0 : 2)}'; + final double durationHours = _durationHours(); + final double estimatedTotal = order.hourlyRate * durationHours; final String dateRange = '${_formatDateShort(schedule.startDate)} - ${_formatDateShort(schedule.endDate)}'; final String timeRange = - '${DateFormat('h:mm a').format(schedule.firstShiftStartsAt)} - ${DateFormat('h:mm a').format(schedule.lastShiftEndsAt)}'; + '${_formatTime(schedule.firstShiftStartsAt)} - ${_formatTime(schedule.lastShiftEndsAt)}'; return Container( margin: const EdgeInsets.only(bottom: UiConstants.space3), @@ -89,8 +104,8 @@ class AvailableOrderCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // -- Badge row: order type, instant book, dispatch team -- - _buildBadgeRow(), + // -- Badge row -- + _buildBadgeRow(spotsLeft), const SizedBox(height: UiConstants.space3), // -- Main content row: icon + details + pay -- @@ -99,176 +114,59 @@ class AvailableOrderCard extends StatelessWidget { children: [ // Role icon Container( - width: 44, - height: 44, + width: UiConstants.space10, + height: UiConstants.space10, decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - UiColors.primary.withValues(alpha: 0.09), - UiColors.primary.withValues(alpha: 0.03), - ], - ), - borderRadius: - BorderRadius.circular(UiConstants.radiusBase), + color: UiColors.tagInProgress, + borderRadius: UiConstants.radiusLg, ), child: const Center( child: Icon( UiIcons.briefcase, color: UiColors.primary, - size: UiConstants.iconMd, + size: UiConstants.space5, ), ), ), const SizedBox(width: UiConstants.space3), - // Details + // Details + pay Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Role name + hourly rate + // Role name + estimated total Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space1, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - order.roleName, - style: UiTypography.body2m.textPrimary, - overflow: TextOverflow.ellipsis, - ), - if (order.clientName.isNotEmpty) - Text( - order.clientName, - style: UiTypography.body3r.textSecondary, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - const SizedBox(width: UiConstants.space2), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '$hourlyDisplay${t.available_orders.per_hour}', - style: UiTypography.title1m.textPrimary, - ), - Text( - '${order.filledCount}/${order.requiredWorkerCount} ${t.available_orders.spots_left(count: spotsLeft)}', - style: UiTypography.footnote2r.textSecondary, - ), - ], - ), - ], - ), - const SizedBox(height: UiConstants.space2), - - // Location - if (order.location.isNotEmpty) - Padding( - padding: - const EdgeInsets.only(bottom: UiConstants.space1), - child: Row( - children: [ - const Icon( - UiIcons.mapPin, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Expanded( - child: Text( - order.location, - style: UiTypography.footnote1r.textSecondary, - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - ), - - // Address - if (order.locationAddress.isNotEmpty) - Padding( - padding: - const EdgeInsets.only(bottom: UiConstants.space4), - child: Padding( - padding: const EdgeInsets.only( - left: UiConstants.iconXs + UiConstants.space1, - ), + Flexible( child: Text( - order.locationAddress, - style: UiTypography.footnote2r.textSecondary, + order.roleName, + style: UiTypography.body1m.textPrimary, overflow: TextOverflow.ellipsis, - maxLines: 1, ), ), - ), - - - // Schedule: days of week chips - if (schedule.daysOfWeek.isNotEmpty) - Padding( - padding: - const EdgeInsets.only(bottom: UiConstants.space2), - child: Wrap( - spacing: UiConstants.space1, - runSpacing: UiConstants.space1, - children: schedule.daysOfWeek - .map( - (DayOfWeek day) => _buildDayChip(day), - ) - .toList(), - ), - ), - - // Date range + time + shifts count - Column( - children: [ - const SizedBox(height: UiConstants.space2), - Row( - children: [ - const Icon( - UiIcons.calendar, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Text( - dateRange, - style: UiTypography.footnote1r.textSecondary, - ), - ], - ), - const SizedBox(width: UiConstants.space2), - Row( - children: [ - const Icon( - UiIcons.clock, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Text( - timeRange, - style: UiTypography.footnote1r.textSecondary, - ), - ], + Text( + '\$${estimatedTotal.toStringAsFixed(0)}', + style: UiTypography.title1m.textPrimary, ), ], ), - const SizedBox(height: UiConstants.space1), - - // Total shifts count - Text( - t.available_orders.shifts_count( - count: schedule.totalShifts, - ), - style: UiTypography.footnote2r.textSecondary, + // Time subtitle + hourly rate + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: UiConstants.space1, + children: [ + Text( + timeRange, + style: UiTypography.body3r.textSecondary, + ), + Text( + '\$${order.hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h', + style: UiTypography.footnote2r.textSecondary, + ), + ], ), ], ), @@ -277,6 +175,87 @@ class AvailableOrderCard extends StatelessWidget { ), const SizedBox(height: UiConstants.space3), + // -- Date -- + Row( + children: [ + const Icon( + UiIcons.calendar, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Text( + dateRange, + style: UiTypography.body3r.textSecondary, + ), + ], + ), + const SizedBox(height: UiConstants.space1), + + // -- Client name -- + if (order.clientName.isNotEmpty) + Row( + children: [ + const Icon( + UiIcons.building, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + order.clientName, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + + // -- Address -- + if (order.locationAddress.isNotEmpty) ...[ + const SizedBox(height: UiConstants.space1), + Row( + children: [ + const Icon( + UiIcons.mapPin, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + order.locationAddress, + style: UiTypography.body3r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + + // -- Schedule: days of week chips -- + if (schedule.daysOfWeek.isNotEmpty) ...[ + const SizedBox(height: UiConstants.space3), + Wrap( + spacing: UiConstants.space1, + runSpacing: UiConstants.space1, + children: schedule.daysOfWeek + .map((DayOfWeek day) => _buildDayChip(day)) + .toList(), + ), + const SizedBox(height: UiConstants.space1), + Text( + t.available_orders.shifts_count( + count: schedule.totalShifts, + ), + style: UiTypography.footnote2r.textSecondary, + ), + ], + + const SizedBox(height: UiConstants.space3), + // -- Action button -- SizedBox( width: double.infinity, @@ -322,7 +301,7 @@ class AvailableOrderCard extends StatelessWidget { } /// Builds the horizontal row of badge chips at the top of the card. - Widget _buildBadgeRow() { + Widget _buildBadgeRow(int spotsLeft) { return Wrap( spacing: UiConstants.space2, runSpacing: UiConstants.space1, @@ -335,6 +314,15 @@ class AvailableOrderCard extends StatelessWidget { borderColor: UiColors.border, ), + // Spots left badge + if (spotsLeft > 0) + _buildBadge( + label: t.available_orders.spots_left(count: spotsLeft), + backgroundColor: UiColors.tagPending, + textColor: UiColors.textWarning, + borderColor: UiColors.textWarning.withValues(alpha: 0.3), + ), + // Instant book badge if (order.instantBook) _buildBadge( @@ -393,7 +381,6 @@ class AvailableOrderCard extends StatelessWidget { /// Builds a small chip showing a day-of-week abbreviation. Widget _buildDayChip(DayOfWeek day) { - // Display as 3-letter capitalised abbreviation (e.g. "MON" -> "Mon"). final String label = day.value.isNotEmpty ? '${day.value[0]}${day.value.substring(1).toLowerCase()}' : '';