diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json index dfa91360..bfbf59ef 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/en.i18n.json @@ -418,7 +418,8 @@ "total": "Total", "estimated_total": "Estimated Total", "estimated_weekly_total": "Estimated Weekly Total", - "post_order": "Post Order" + "post_order": "Post Order", + "hours_suffix": "hrs" }, "rapid_draft": { "title": "Rapid Order", diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json index 29dffc83..1d8e5bc7 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json +++ b/apps/mobile/packages/core_localization/lib/src/l10n/es.i18n.json @@ -418,7 +418,8 @@ "total": "Total", "estimated_total": "Total Estimado", "estimated_weekly_total": "Total Semanal Estimado", - "post_order": "Publicar Orden" + "post_order": "Publicar Orden", + "hours_suffix": "hrs" }, "rapid_draft": { "title": "Orden R\u00e1pida", diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart index 9d1e4bec..8e272bb9 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/one_time_order_page.dart @@ -10,6 +10,7 @@ import '../blocs/one_time_order/one_time_order_bloc.dart'; import '../blocs/one_time_order/one_time_order_event.dart'; import '../blocs/one_time_order/one_time_order_state.dart'; import '../models/review_order_arguments.dart'; +import '../utils/time_parsing_utils.dart'; import '../widgets/review_order/review_order_positions_card.dart'; /// Page for creating a one-time staffing order. @@ -117,6 +118,9 @@ class OneTimeOrderPage extends StatelessWidget { roleName: state.roleNameById(p.role) ?? p.role, workerCount: p.count, costPerHour: state.roleCostById(p.role), + hours: parseHoursFromTimes(p.startTime, p.endTime), + startTime: p.startTime, + endTime: p.endTime, ), ).toList(); diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart index c15dcdb6..331c76b6 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/permanent_order_page.dart @@ -11,6 +11,7 @@ import '../blocs/permanent_order/permanent_order_event.dart'; import '../blocs/permanent_order/permanent_order_state.dart'; import '../models/review_order_arguments.dart'; import '../utils/schedule_utils.dart'; +import '../utils/time_parsing_utils.dart'; import '../widgets/review_order/review_order_positions_card.dart'; /// Page for creating a permanent staffing order. @@ -129,6 +130,9 @@ class PermanentOrderPage extends StatelessWidget { roleName: state.roleNameById(p.role) ?? p.role, workerCount: p.count, costPerHour: state.roleCostById(p.role), + hours: parseHoursFromTimes(p.startTime, p.endTime), + startTime: p.startTime, + endTime: p.endTime, ), ).toList(); diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart index c7fe4979..c092b12e 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/recurring_order_page.dart @@ -10,6 +10,7 @@ import '../blocs/recurring_order/recurring_order_event.dart'; import '../blocs/recurring_order/recurring_order_state.dart'; import '../models/review_order_arguments.dart'; import '../utils/schedule_utils.dart'; +import '../utils/time_parsing_utils.dart'; import '../widgets/review_order/review_order_positions_card.dart'; /// Page for creating a recurring staffing order. @@ -138,6 +139,9 @@ class RecurringOrderPage extends StatelessWidget { roleName: state.roleNameById(p.role) ?? p.role, workerCount: p.count, costPerHour: state.roleCostById(p.role), + hours: parseHoursFromTimes(p.startTime, p.endTime), + startTime: p.startTime, + endTime: p.endTime, ), ).toList(); diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_positions_card.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_positions_card.dart index 723bbb5a..73b0b09d 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_positions_card.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_positions_card.dart @@ -1,13 +1,16 @@ import 'package:core_localization/core_localization.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; -import 'review_order_info_row.dart'; /// Displays a summary of all positions with a divider and total row. /// -/// Each position shows the role name and "N workers . $X/hr". +/// Each position is rendered as a two-line layout: +/// - Line 1: role name (left) and worker count with cost/hr (right). +/// - Line 2: time range and shift hours (right-aligned, muted style). +/// /// A divider separates the individual positions from the total. class ReviewOrderPositionsCard extends StatelessWidget { + /// Creates a [ReviewOrderPositionsCard]. const ReviewOrderPositionsCard({ required this.positions, required this.totalWorkers, @@ -16,9 +19,16 @@ class ReviewOrderPositionsCard extends StatelessWidget { super.key, }); + /// The list of position items to display. final List positions; + + /// The total number of workers across all positions. final int totalWorkers; + + /// The combined cost per hour across all positions. final double totalCostPerHour; + + /// Optional callback invoked when the user taps "Edit". final VoidCallback? onEdit; @override @@ -50,16 +60,7 @@ class ReviewOrderPositionsCard extends StatelessWidget { ), ], ), - ...positions.map( - (ReviewPositionItem position) => Padding( - padding: const EdgeInsets.only(top: UiConstants.space3), - child: ReviewOrderInfoRow( - label: position.roleName, - value: - '${position.workerCount} workers \u00B7 \$${position.costPerHour.toStringAsFixed(0)}/hr', - ), - ), - ), + ...positions.map(_buildPositionItem), Padding( padding: const EdgeInsets.only(top: UiConstants.space3), child: Container( @@ -74,11 +75,12 @@ class ReviewOrderPositionsCard extends StatelessWidget { children: [ Text( t.client_create_order.review.total, - style: UiTypography.body3m, + style: UiTypography.body2m, ), Text( - '$totalWorkers workers \u00B7 \$${totalCostPerHour.toStringAsFixed(0)}/hr', - style: UiTypography.body3b.primary, + '$totalWorkers workers \u00B7 ' + '\$${totalCostPerHour.toStringAsFixed(0)}/hr', + style: UiTypography.body2b.primary, ), ], ), @@ -87,17 +89,83 @@ class ReviewOrderPositionsCard extends StatelessWidget { ), ); } + + /// Builds a two-line widget for a single position. + /// + /// Line 1 shows the role name on the left and worker count with cost on + /// the right. Line 2 shows the time range and shift hours, right-aligned + /// in a secondary/muted style. + Widget _buildPositionItem(ReviewPositionItem position) { + final String formattedHours = position.hours % 1 == 0 + ? position.hours.toInt().toString() + : position.hours.toStringAsFixed(1); + + return Padding( + padding: const EdgeInsets.only(top: UiConstants.space3), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + position.roleName, + style: UiTypography.body2m.textSecondary, + ), + ), + Text( + '${position.workerCount} workers \u00B7 ' + '\$${position.costPerHour.toStringAsFixed(0)}/hr', + style: UiTypography.body2m, + ), + ], + ), + const SizedBox(height: UiConstants.space1), + Align( + alignment: Alignment.centerRight, + child: Text( + '${position.startTime} - ${position.endTime} \u00B7 ' + '$formattedHours ' + '${t.client_create_order.review.hours_suffix}', + style: UiTypography.body3r.textTertiary, + ), + ), + ], + ), + ); + } } /// A single position item for the positions card. +/// +/// Contains the role name, worker count, shift hours, hourly cost, +/// and the start/end times for one position in the review summary. class ReviewPositionItem { + /// Creates a [ReviewPositionItem]. const ReviewPositionItem({ required this.roleName, required this.workerCount, required this.costPerHour, + required this.hours, + required this.startTime, + required this.endTime, }); + /// The display name of the role for this position. final String roleName; + + /// The number of workers requested for this position. final int workerCount; + + /// The cost per hour for this role. final double costPerHour; + + /// The number of shift hours (derived from start/end time). + final double hours; + + /// The formatted start time of the shift (e.g. "08:00 AM"). + final String startTime; + + /// The formatted end time of the shift (e.g. "04:00 PM"). + final String endTime; }