feat: Add estimated weekly total label and refactor cost calculations for one-time and recurring orders

This commit is contained in:
Achintha Isuru
2026-03-10 10:31:08 -04:00
parent a3aab678fd
commit 48207367cb
10 changed files with 64 additions and 9 deletions

View File

@@ -417,6 +417,7 @@
"positions": "POSITIONS", "positions": "POSITIONS",
"total": "Total", "total": "Total",
"estimated_total": "Estimated Total", "estimated_total": "Estimated Total",
"estimated_weekly_total": "Estimated Weekly Total",
"post_order": "Post Order" "post_order": "Post Order"
}, },
"rapid_draft": { "rapid_draft": {

View File

@@ -417,6 +417,7 @@
"positions": "POSICIONES", "positions": "POSICIONES",
"total": "Total", "total": "Total",
"estimated_total": "Total Estimado", "estimated_total": "Total Estimado",
"estimated_weekly_total": "Total Semanal Estimado",
"post_order": "Publicar Orden" "post_order": "Publicar Orden"
}, },
"rapid_draft": { "rapid_draft": {

View File

@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../utils/time_parsing_utils.dart'; import '../../utils/time_parsing_utils.dart';
enum OneTimeOrderStatus { initial, loading, success, failure } enum OneTimeOrderStatus { initial, loading, success, failure }
@@ -162,9 +163,8 @@ class OneTimeOrderState extends Equatable {
buffer.write('$minutes min'); buffer.write('$minutes min');
} }
if (first.lunchBreak != null && if (first.lunchBreak != 'NO_BREAK' &&
first.lunchBreak != 'NO_BREAK' && first.lunchBreak.isNotEmpty) {
first.lunchBreak!.isNotEmpty) {
buffer.write(' (${first.lunchBreak} break)'); buffer.write(' (${first.lunchBreak} break)');
} }

View File

@@ -148,8 +148,8 @@ class PermanentOrderState extends Equatable {
sum + (p.count * roleCostById(p.role)), sum + (p.count * roleCostById(p.role)),
); );
/// Estimated total cost: sum of (count * costPerHour * hours) per position. /// Daily cost: sum of (count * costPerHour * hours) per position.
double get estimatedTotal { double get dailyCost {
double total = 0; double total = 0;
for (final PermanentOrderPosition p in positions) { for (final PermanentOrderPosition p in positions) {
final double hours = parseHoursFromTimes(p.startTime, p.endTime); final double hours = parseHoursFromTimes(p.startTime, p.endTime);
@@ -158,6 +158,12 @@ class PermanentOrderState extends Equatable {
return total; return total;
} }
/// Estimated weekly total cost for the permanent order.
///
/// Calculated as [dailyCost] multiplied by the number of selected
/// [permanentDays] per week.
double get estimatedTotal => dailyCost * permanentDays.length;
/// Formatted repeat days (e.g. "Mon, Tue, Wed"). /// Formatted repeat days (e.g. "Mon, Tue, Wed").
String get formattedRepeatDays => permanentDays.map( String get formattedRepeatDays => permanentDays.map(
(String day) => day[0] + day.substring(1).toLowerCase(), (String day) => day[0] + day.substring(1).toLowerCase(),

View File

@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../../utils/schedule_utils.dart';
import '../../utils/time_parsing_utils.dart'; import '../../utils/time_parsing_utils.dart';
enum RecurringOrderStatus { initial, loading, success, failure } enum RecurringOrderStatus { initial, loading, success, failure }
@@ -155,8 +156,8 @@ class RecurringOrderState extends Equatable {
sum + (p.count * roleCostById(p.role)), sum + (p.count * roleCostById(p.role)),
); );
/// Estimated total cost: sum of (count * costPerHour * hours) per position. /// Daily cost: sum of (count * costPerHour * hours) per position.
double get estimatedTotal { double get dailyCost {
double total = 0; double total = 0;
for (final RecurringOrderPosition p in positions) { for (final RecurringOrderPosition p in positions) {
final double hours = parseHoursFromTimes(p.startTime, p.endTime); final double hours = parseHoursFromTimes(p.startTime, p.endTime);
@@ -165,6 +166,31 @@ class RecurringOrderState extends Equatable {
return total; return total;
} }
/// Total number of working days between [startDate] and [endDate]
/// (inclusive) that match the selected [recurringDays].
///
/// Iterates day-by-day and counts each date whose weekday label
/// (e.g. "MON", "TUE") appears in [recurringDays].
int get totalWorkingDays {
final Set<String> selectedSet = recurringDays.toSet();
int count = 0;
for (
DateTime day = startDate;
!day.isAfter(endDate);
day = day.add(const Duration(days: 1))
) {
if (selectedSet.contains(weekdayLabel(day))) {
count++;
}
}
return count;
}
/// Estimated total cost for the entire recurring order period.
///
/// Calculated as [dailyCost] multiplied by [totalWorkingDays].
double get estimatedTotal => dailyCost * totalWorkingDays;
/// Formatted repeat days (e.g. "Mon, Tue, Wed"). /// Formatted repeat days (e.g. "Mon, Tue, Wed").
String get formattedRepeatDays => recurringDays.map( String get formattedRepeatDays => recurringDays.map(
(String day) => day[0] + day.substring(1).toLowerCase(), (String day) => day[0] + day.substring(1).toLowerCase(),

View File

@@ -25,6 +25,7 @@ class ReviewOrderArguments {
this.scheduleStartDate, this.scheduleStartDate,
this.scheduleEndDate, this.scheduleEndDate,
this.scheduleRepeatDays, this.scheduleRepeatDays,
this.totalLabel,
}); });
final ReviewOrderType orderType; final ReviewOrderType orderType;
@@ -45,4 +46,7 @@ class ReviewOrderArguments {
final String? scheduleStartDate; final String? scheduleStartDate;
final String? scheduleEndDate; final String? scheduleEndDate;
final String? scheduleRepeatDays; final String? scheduleRepeatDays;
/// Optional label override for the total banner (e.g. "Estimated Weekly Total").
final String? totalLabel;
} }

View File

@@ -1,3 +1,4 @@
import 'package:core_localization/core_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
@@ -143,6 +144,7 @@ class PermanentOrderPage extends StatelessWidget {
estimatedTotal: state.estimatedTotal, estimatedTotal: state.estimatedTotal,
scheduleStartDate: DateFormat.yMMMd().format(state.startDate), scheduleStartDate: DateFormat.yMMMd().format(state.startDate),
scheduleRepeatDays: state.formattedRepeatDays, scheduleRepeatDays: state.formattedRepeatDays,
totalLabel: t.client_create_order.review.estimated_weekly_total,
), ),
); );

View File

@@ -52,6 +52,7 @@ class ReviewOrderPage extends StatelessWidget {
totalWorkers: args.totalWorkers, totalWorkers: args.totalWorkers,
totalCostPerHour: args.totalCostPerHour, totalCostPerHour: args.totalCostPerHour,
estimatedTotal: args.estimatedTotal, estimatedTotal: args.estimatedTotal,
totalLabel: args.totalLabel,
showEditButtons: showEdit, showEditButtons: showEdit,
onEditBasics: showEdit ? () => Modular.to.popSafe() : null, onEditBasics: showEdit ? () => Modular.to.popSafe() : null,
onEditSchedule: showEdit ? () => Modular.to.popSafe() : null, onEditSchedule: showEdit ? () => Modular.to.popSafe() : null,

View File

@@ -5,14 +5,20 @@ import 'package:flutter/material.dart';
/// A highlighted banner displaying the estimated total cost. /// A highlighted banner displaying the estimated total cost.
/// ///
/// Uses the primary inverse background color with a bold price display. /// Uses the primary inverse background color with a bold price display.
/// An optional [label] can override the default "Estimated Total" text.
class ReviewOrderTotalBanner extends StatelessWidget { class ReviewOrderTotalBanner extends StatelessWidget {
const ReviewOrderTotalBanner({ const ReviewOrderTotalBanner({
required this.totalAmount, required this.totalAmount,
this.label,
super.key, super.key,
}); });
/// The total monetary amount to display.
final double totalAmount; final double totalAmount;
/// Optional label override. Defaults to the localized "Estimated Total".
final String? label;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@@ -28,7 +34,7 @@ class ReviewOrderTotalBanner extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[ children: <Widget>[
Text( Text(
t.client_create_order.review.estimated_total, label ?? t.client_create_order.review.estimated_total,
style: UiTypography.body2m, style: UiTypography.body2m,
), ),
Text( Text(

View File

@@ -32,6 +32,7 @@ class ReviewOrderView extends StatelessWidget {
this.onEditSchedule, this.onEditSchedule,
this.onEditPositions, this.onEditPositions,
this.submitLabel, this.submitLabel,
this.totalLabel,
this.isLoading = false, this.isLoading = false,
super.key, super.key,
}); });
@@ -51,6 +52,10 @@ class ReviewOrderView extends StatelessWidget {
final VoidCallback? onEditSchedule; final VoidCallback? onEditSchedule;
final VoidCallback? onEditPositions; final VoidCallback? onEditPositions;
final String? submitLabel; final String? submitLabel;
/// Optional label override for the total banner. When `null`, the default
/// localized "Estimated Total" text is used.
final String? totalLabel;
final bool isLoading; final bool isLoading;
@override @override
@@ -92,7 +97,10 @@ class ReviewOrderView extends StatelessWidget {
onEdit: showEditButtons ? onEditPositions : null, onEdit: showEditButtons ? onEditPositions : null,
), ),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
ReviewOrderTotalBanner(totalAmount: estimatedTotal), ReviewOrderTotalBanner(
totalAmount: estimatedTotal,
label: totalLabel,
),
const SizedBox(height: UiConstants.space4), const SizedBox(height: UiConstants.space4),
], ],
), ),