feat: Enhance review order summary with hours display and localization for hours suffix
This commit is contained in:
@@ -418,7 +418,8 @@
|
|||||||
"total": "Total",
|
"total": "Total",
|
||||||
"estimated_total": "Estimated Total",
|
"estimated_total": "Estimated Total",
|
||||||
"estimated_weekly_total": "Estimated Weekly Total",
|
"estimated_weekly_total": "Estimated Weekly Total",
|
||||||
"post_order": "Post Order"
|
"post_order": "Post Order",
|
||||||
|
"hours_suffix": "hrs"
|
||||||
},
|
},
|
||||||
"rapid_draft": {
|
"rapid_draft": {
|
||||||
"title": "Rapid Order",
|
"title": "Rapid Order",
|
||||||
|
|||||||
@@ -418,7 +418,8 @@
|
|||||||
"total": "Total",
|
"total": "Total",
|
||||||
"estimated_total": "Total Estimado",
|
"estimated_total": "Total Estimado",
|
||||||
"estimated_weekly_total": "Total Semanal Estimado",
|
"estimated_weekly_total": "Total Semanal Estimado",
|
||||||
"post_order": "Publicar Orden"
|
"post_order": "Publicar Orden",
|
||||||
|
"hours_suffix": "hrs"
|
||||||
},
|
},
|
||||||
"rapid_draft": {
|
"rapid_draft": {
|
||||||
"title": "Orden R\u00e1pida",
|
"title": "Orden R\u00e1pida",
|
||||||
|
|||||||
@@ -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_event.dart';
|
||||||
import '../blocs/one_time_order/one_time_order_state.dart';
|
import '../blocs/one_time_order/one_time_order_state.dart';
|
||||||
import '../models/review_order_arguments.dart';
|
import '../models/review_order_arguments.dart';
|
||||||
|
import '../utils/time_parsing_utils.dart';
|
||||||
import '../widgets/review_order/review_order_positions_card.dart';
|
import '../widgets/review_order/review_order_positions_card.dart';
|
||||||
|
|
||||||
/// Page for creating a one-time staffing order.
|
/// Page for creating a one-time staffing order.
|
||||||
@@ -117,6 +118,9 @@ class OneTimeOrderPage extends StatelessWidget {
|
|||||||
roleName: state.roleNameById(p.role) ?? p.role,
|
roleName: state.roleNameById(p.role) ?? p.role,
|
||||||
workerCount: p.count,
|
workerCount: p.count,
|
||||||
costPerHour: state.roleCostById(p.role),
|
costPerHour: state.roleCostById(p.role),
|
||||||
|
hours: parseHoursFromTimes(p.startTime, p.endTime),
|
||||||
|
startTime: p.startTime,
|
||||||
|
endTime: p.endTime,
|
||||||
),
|
),
|
||||||
).toList();
|
).toList();
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../blocs/permanent_order/permanent_order_event.dart';
|
|||||||
import '../blocs/permanent_order/permanent_order_state.dart';
|
import '../blocs/permanent_order/permanent_order_state.dart';
|
||||||
import '../models/review_order_arguments.dart';
|
import '../models/review_order_arguments.dart';
|
||||||
import '../utils/schedule_utils.dart';
|
import '../utils/schedule_utils.dart';
|
||||||
|
import '../utils/time_parsing_utils.dart';
|
||||||
import '../widgets/review_order/review_order_positions_card.dart';
|
import '../widgets/review_order/review_order_positions_card.dart';
|
||||||
|
|
||||||
/// Page for creating a permanent staffing order.
|
/// Page for creating a permanent staffing order.
|
||||||
@@ -129,6 +130,9 @@ class PermanentOrderPage extends StatelessWidget {
|
|||||||
roleName: state.roleNameById(p.role) ?? p.role,
|
roleName: state.roleNameById(p.role) ?? p.role,
|
||||||
workerCount: p.count,
|
workerCount: p.count,
|
||||||
costPerHour: state.roleCostById(p.role),
|
costPerHour: state.roleCostById(p.role),
|
||||||
|
hours: parseHoursFromTimes(p.startTime, p.endTime),
|
||||||
|
startTime: p.startTime,
|
||||||
|
endTime: p.endTime,
|
||||||
),
|
),
|
||||||
).toList();
|
).toList();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../blocs/recurring_order/recurring_order_event.dart';
|
|||||||
import '../blocs/recurring_order/recurring_order_state.dart';
|
import '../blocs/recurring_order/recurring_order_state.dart';
|
||||||
import '../models/review_order_arguments.dart';
|
import '../models/review_order_arguments.dart';
|
||||||
import '../utils/schedule_utils.dart';
|
import '../utils/schedule_utils.dart';
|
||||||
|
import '../utils/time_parsing_utils.dart';
|
||||||
import '../widgets/review_order/review_order_positions_card.dart';
|
import '../widgets/review_order/review_order_positions_card.dart';
|
||||||
|
|
||||||
/// Page for creating a recurring staffing order.
|
/// Page for creating a recurring staffing order.
|
||||||
@@ -138,6 +139,9 @@ class RecurringOrderPage extends StatelessWidget {
|
|||||||
roleName: state.roleNameById(p.role) ?? p.role,
|
roleName: state.roleNameById(p.role) ?? p.role,
|
||||||
workerCount: p.count,
|
workerCount: p.count,
|
||||||
costPerHour: state.roleCostById(p.role),
|
costPerHour: state.roleCostById(p.role),
|
||||||
|
hours: parseHoursFromTimes(p.startTime, p.endTime),
|
||||||
|
startTime: p.startTime,
|
||||||
|
endTime: p.endTime,
|
||||||
),
|
),
|
||||||
).toList();
|
).toList();
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'review_order_info_row.dart';
|
|
||||||
|
|
||||||
/// Displays a summary of all positions with a divider and total row.
|
/// 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.
|
/// A divider separates the individual positions from the total.
|
||||||
class ReviewOrderPositionsCard extends StatelessWidget {
|
class ReviewOrderPositionsCard extends StatelessWidget {
|
||||||
|
/// Creates a [ReviewOrderPositionsCard].
|
||||||
const ReviewOrderPositionsCard({
|
const ReviewOrderPositionsCard({
|
||||||
required this.positions,
|
required this.positions,
|
||||||
required this.totalWorkers,
|
required this.totalWorkers,
|
||||||
@@ -16,9 +19,16 @@ class ReviewOrderPositionsCard extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// The list of position items to display.
|
||||||
final List<ReviewPositionItem> positions;
|
final List<ReviewPositionItem> positions;
|
||||||
|
|
||||||
|
/// The total number of workers across all positions.
|
||||||
final int totalWorkers;
|
final int totalWorkers;
|
||||||
|
|
||||||
|
/// The combined cost per hour across all positions.
|
||||||
final double totalCostPerHour;
|
final double totalCostPerHour;
|
||||||
|
|
||||||
|
/// Optional callback invoked when the user taps "Edit".
|
||||||
final VoidCallback? onEdit;
|
final VoidCallback? onEdit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -50,16 +60,7 @@ class ReviewOrderPositionsCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
...positions.map(
|
...positions.map(_buildPositionItem),
|
||||||
(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',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: UiConstants.space3),
|
padding: const EdgeInsets.only(top: UiConstants.space3),
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -74,11 +75,12 @@ class ReviewOrderPositionsCard extends StatelessWidget {
|
|||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
t.client_create_order.review.total,
|
t.client_create_order.review.total,
|
||||||
style: UiTypography.body3m,
|
style: UiTypography.body2m,
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'$totalWorkers workers \u00B7 \$${totalCostPerHour.toStringAsFixed(0)}/hr',
|
'$totalWorkers workers \u00B7 '
|
||||||
style: UiTypography.body3b.primary,
|
'\$${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.
|
/// 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 {
|
class ReviewPositionItem {
|
||||||
|
/// Creates a [ReviewPositionItem].
|
||||||
const ReviewPositionItem({
|
const ReviewPositionItem({
|
||||||
required this.roleName,
|
required this.roleName,
|
||||||
required this.workerCount,
|
required this.workerCount,
|
||||||
required this.costPerHour,
|
required this.costPerHour,
|
||||||
|
required this.hours,
|
||||||
|
required this.startTime,
|
||||||
|
required this.endTime,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// The display name of the role for this position.
|
||||||
final String roleName;
|
final String roleName;
|
||||||
|
|
||||||
|
/// The number of workers requested for this position.
|
||||||
final int workerCount;
|
final int workerCount;
|
||||||
|
|
||||||
|
/// The cost per hour for this role.
|
||||||
final double costPerHour;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user