feat: Enhance review order summary with hours display and localization for hours suffix

This commit is contained in:
Achintha Isuru
2026-03-10 10:44:01 -04:00
parent 48207367cb
commit 0d241844dd
6 changed files with 99 additions and 17 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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();

View File

@@ -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();

View File

@@ -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();

View File

@@ -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<ReviewPositionItem> 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: <Widget>[
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: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
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;
}