From 0ff2949c1e24f26db82a1ac2bb1b7d88b083a0b6 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Thu, 19 Mar 2026 21:13:35 -0400 Subject: [PATCH] feat: Refactor ShiftCard components to include client name and improve layout consistency --- .../widgets/available_order_card.dart | 2 +- .../widgets/shift_card/shift_card_body.dart | 47 +++---- .../widgets/shift_card/shift_card_data.dart | 10 ++ .../shift_card/shift_card_metadata_rows.dart | 66 ++++++---- .../shift_card/shift_card_title_row.dart | 115 +++++++++--------- 5 files changed, 125 insertions(+), 115 deletions(-) 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 351f99e1..42fc4b60 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 @@ -135,7 +135,7 @@ class AvailableOrderCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Role name + pay headline + chevron + // Role name + pay headline Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, spacing: UiConstants.space1, diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_body.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_body.dart index afad825c..7573f53c 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_body.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_body.dart @@ -14,27 +14,23 @@ class ShiftCardBody extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ShiftCardIcon(variant: data.variant), - const SizedBox(width: UiConstants.space3), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ShiftCardTitleRow(data: data), - const SizedBox(height: UiConstants.space2), - ShiftCardMetadataRows(data: data), - ], - ), + Row( + children: [ + ShiftCardIcon(variant: data.variant), + ShiftCardTitleRow(data: data), + ], ), + const SizedBox(height: UiConstants.space2), + ShiftCardMetadataRows(data: data), ], ); } } -/// The 44x44 icon box with a gradient background. +/// The icon box matching the AvailableOrderCard style. class ShiftCardIcon extends StatelessWidget { /// Creates a [ShiftCardIcon]. const ShiftCardIcon({super.key, required this.variant}); @@ -47,30 +43,19 @@ class ShiftCardIcon extends StatelessWidget { final bool isCancelled = variant == ShiftCardVariant.cancelled; return Container( - width: 44, - height: 44, + width: UiConstants.space10, + height: UiConstants.space10, decoration: BoxDecoration( - gradient: isCancelled - ? null - : LinearGradient( - colors: [ - UiColors.primary.withValues(alpha: 0.09), - UiColors.primary.withValues(alpha: 0.03), - ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, - ), - color: isCancelled ? UiColors.primary.withValues(alpha: 0.05) : null, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - border: isCancelled - ? null - : Border.all(color: UiColors.primary.withValues(alpha: 0.09)), + color: isCancelled + ? UiColors.primary.withValues(alpha: 0.05) + : UiColors.tagInProgress, + borderRadius: UiConstants.radiusLg, ), child: const Center( child: Icon( UiIcons.briefcase, color: UiColors.primary, - size: UiConstants.iconMd, + size: UiConstants.space5, ), ), ); diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_data.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_data.dart index 626ff583..2e029ffa 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_data.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_data.dart @@ -38,6 +38,7 @@ class ShiftCardData { required this.date, required this.variant, this.subtitle, + this.clientName, this.startTime, this.endTime, this.hourlyRateCents, @@ -57,9 +58,12 @@ class ShiftCardData { subtitle: shift.location, location: shift.location, date: shift.date, + clientName: shift.clientName, startTime: shift.startTime, endTime: shift.endTime, hourlyRateCents: shift.hourlyRateCents, + hourlyRate: shift.hourlyRate, + totalRate: shift.totalRate, orderType: shift.orderType, variant: _variantFromAssignmentStatus(shift.status), ); @@ -73,6 +77,7 @@ class ShiftCardData { subtitle: shift.title.isNotEmpty ? shift.title : null, location: shift.location, date: shift.date, + clientName: shift.clientName, startTime: shift.startTime, endTime: shift.endTime, hourlyRateCents: shift.hourlyRateCents, @@ -91,6 +96,7 @@ class ShiftCardData { title: shift.title, location: shift.location, date: shift.date, + clientName: shift.clientName, cancellationReason: shift.cancellationReason, variant: ShiftCardVariant.cancelled, ); @@ -104,6 +110,7 @@ class ShiftCardData { subtitle: assignment.title.isNotEmpty ? assignment.title : null, location: assignment.location, date: assignment.startTime, + clientName: assignment.clientName, startTime: assignment.startTime, endTime: assignment.endTime, variant: ShiftCardVariant.pending, @@ -119,6 +126,9 @@ class ShiftCardData { /// Optional secondary text (e.g. location under the role name). final String? subtitle; + /// Client/business name. + final String? clientName; + /// Human-readable location label. final String location; diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_metadata_rows.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_metadata_rows.dart index df0ce572..c7416145 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_metadata_rows.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_metadata_rows.dart @@ -4,7 +4,10 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_data.dart'; -/// Date, time, location, and worked-hours rows. +/// Date, client name, location, and worked-hours metadata rows. +/// +/// Follows the AvailableOrderCard element ordering: +/// date -> client name -> location. class ShiftCardMetadataRows extends StatelessWidget { /// Creates a [ShiftCardMetadataRows]. const ShiftCardMetadataRows({super.key, required this.data}); @@ -15,62 +18,71 @@ class ShiftCardMetadataRows extends StatelessWidget { @override Widget build(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Date and time row + // Date row (with optional worked duration for completed shifts). Row( children: [ const Icon( UiIcons.calendar, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, + size: UiConstants.space3, + color: UiColors.mutedForeground, ), const SizedBox(width: UiConstants.space1), Text( _formatDate(context, data.date), - style: UiTypography.footnote1r.textSecondary, + style: UiTypography.body3r.textSecondary, ), - if (data.startTime != null && data.endTime != null) ...[ - const SizedBox(width: UiConstants.space3), - const Icon( - UiIcons.clock, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, - ), - const SizedBox(width: UiConstants.space1), - Text( - '${_formatTime(data.startTime!)} - ${_formatTime(data.endTime!)}', - style: UiTypography.footnote1r.textSecondary, - ), - ], if (data.minutesWorked != null) ...[ const SizedBox(width: UiConstants.space3), const Icon( UiIcons.clock, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, + size: UiConstants.space3, + color: UiColors.mutedForeground, ), const SizedBox(width: UiConstants.space1), Text( _formatWorkedDuration(data.minutesWorked!), - style: UiTypography.footnote1r.textSecondary, + style: UiTypography.body3r.textSecondary, ), ], ], ), + // Client name row. + if (data.clientName != null && data.clientName!.isNotEmpty) ...[ + const SizedBox(height: UiConstants.space1), + Row( + children: [ + const Icon( + UiIcons.building, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + data.clientName!, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + // Location row. const SizedBox(height: UiConstants.space1), - // Location row Row( children: [ const Icon( UiIcons.mapPin, - size: UiConstants.iconXs, - color: UiColors.iconSecondary, + size: UiConstants.space3, + color: UiColors.mutedForeground, ), const SizedBox(width: UiConstants.space1), Expanded( child: Text( data.location, - style: UiTypography.footnote1r.textSecondary, + style: UiTypography.body3r.textSecondary, overflow: TextOverflow.ellipsis, ), ), @@ -80,6 +92,7 @@ class ShiftCardMetadataRows extends StatelessWidget { ); } + /// Formats [date] relative to today/tomorrow, or as "EEE, MMM d". String _formatDate(BuildContext context, DateTime date) { final DateTime now = DateTime.now(); final DateTime today = DateTime(now.year, now.month, now.day); @@ -92,8 +105,7 @@ class ShiftCardMetadataRows extends StatelessWidget { return DateFormat('EEE, MMM d').format(date); } - String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt); - + /// Formats total minutes worked into a "Xh Ym" string. String _formatWorkedDuration(int totalMinutes) { final int hours = totalMinutes ~/ 60; final int mins = totalMinutes % 60; diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_title_row.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_title_row.dart index f6b18b07..77c2ac4c 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_title_row.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_card/shift_card_title_row.dart @@ -1,8 +1,10 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:staff_shifts/src/presentation/widgets/shift_card/shift_card_data.dart'; -/// Title row with optional pay summary on the right. +/// Title row showing role name + pay headline, with a time subtitle + pay detail +/// row below. Matches the AvailableOrderCard layout. class ShiftCardTitleRow extends StatelessWidget { /// Creates a [ShiftCardTitleRow]. const ShiftCardTitleRow({super.key, required this.data}); @@ -12,77 +14,78 @@ class ShiftCardTitleRow extends StatelessWidget { @override Widget build(BuildContext context) { + // Determine if we have enough data to show pay information. final bool hasDirectRate = data.hourlyRate != null && data.hourlyRate! > 0; final bool hasComputedRate = data.hourlyRateCents != null && data.startTime != null && data.endTime != null; + final bool hasPay = hasDirectRate || hasComputedRate; - if (!hasDirectRate && !hasComputedRate) { - return Text( - data.title, - style: UiTypography.body2m.textPrimary, - overflow: TextOverflow.ellipsis, - ); + // Compute pay values when available. + double hourlyRate = 0; + double estimatedTotal = 0; + double durationHours = 0; + + if (hasPay) { + if (hasDirectRate && data.totalRate != null && data.totalRate! > 0) { + hourlyRate = data.hourlyRate!; + estimatedTotal = data.totalRate!; + durationHours = hourlyRate > 0 ? (estimatedTotal / hourlyRate) : 0; + } else if (hasComputedRate) { + hourlyRate = data.hourlyRateCents! / 100; + final int durationMinutes = + data.endTime!.difference(data.startTime!).inMinutes; + double hours = durationMinutes / 60; + if (hours < 0) hours += 24; + durationHours = hours.roundToDouble(); + estimatedTotal = hourlyRate * durationHours; + } } - // Prefer pre-computed values from the API when available. - final double hourlyRate; - final double estimatedTotal; - final double durationHours; - - if (hasDirectRate && data.totalRate != null && data.totalRate! > 0) { - hourlyRate = data.hourlyRate!; - estimatedTotal = data.totalRate!; - durationHours = hourlyRate > 0 ? (estimatedTotal / hourlyRate) : 0; - } else { - hourlyRate = data.hourlyRateCents! / 100; - final int durationMinutes = data.endTime! - .difference(data.startTime!) - .inMinutes; - double hours = durationMinutes / 60; - if (hours < 0) hours += 24; - durationHours = hours.roundToDouble(); - estimatedTotal = hourlyRate * durationHours; - } - - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( + // Row 1: Title + Pay headline + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: UiConstants.space1, + children: [ + Flexible( + child: Text( data.title, - style: UiTypography.body2m.textPrimary, + style: UiTypography.body1m.textPrimary, overflow: TextOverflow.ellipsis, ), - if (data.subtitle != null) ...[ - Text( - data.subtitle!, - style: UiTypography.body3r.textSecondary, - overflow: TextOverflow.ellipsis, - ), - ], - ], - ), - ), - const SizedBox(width: UiConstants.space3), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '\$${estimatedTotal.toStringAsFixed(0)}', - style: UiTypography.title1m.textPrimary, - ), - Text( - '\$${hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h', - style: UiTypography.footnote2r.textSecondary, ), + if (hasPay) + Text( + '\$${estimatedTotal.toStringAsFixed(0)}', + style: UiTypography.title1m.textPrimary, + ), ], ), + // Row 2: Time subtitle + pay detail + if (data.startTime != null && data.endTime != null) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: UiConstants.space1, + children: [ + Text( + '${_formatTime(data.startTime!)} - ${_formatTime(data.endTime!)}', + style: UiTypography.body3r.textSecondary, + ), + if (hasPay) + Text( + '\$${hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h', + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), ], ); } + + /// Formats a [DateTime] to a compact time string like "3:30pm". + String _formatTime(DateTime dt) => DateFormat('h:mma').format(dt).toLowerCase(); }