From 48207367cb493fc9266fb48021a9bcf4c01b5732 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Tue, 10 Mar 2026 10:31:08 -0400 Subject: [PATCH] feat: Add estimated weekly total label and refactor cost calculations for one-time and recurring orders --- .../lib/src/l10n/en.i18n.json | 1 + .../lib/src/l10n/es.i18n.json | 1 + .../one_time_order/one_time_order_state.dart | 6 ++-- .../permanent_order_state.dart | 10 +++++-- .../recurring_order_state.dart | 30 +++++++++++++++++-- .../models/review_order_arguments.dart | 4 +++ .../pages/permanent_order_page.dart | 2 ++ .../presentation/pages/review_order_page.dart | 1 + .../review_order_total_banner.dart | 8 ++++- .../review_order/review_order_view.dart | 10 ++++++- 10 files changed, 64 insertions(+), 9 deletions(-) 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 6440bcb8..dfa91360 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 @@ -417,6 +417,7 @@ "positions": "POSITIONS", "total": "Total", "estimated_total": "Estimated Total", + "estimated_weekly_total": "Estimated Weekly Total", "post_order": "Post Order" }, "rapid_draft": { 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 75501f4f..29dffc83 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 @@ -417,6 +417,7 @@ "positions": "POSICIONES", "total": "Total", "estimated_total": "Total Estimado", + "estimated_weekly_total": "Total Semanal Estimado", "post_order": "Publicar Orden" }, "rapid_draft": { diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_state.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_state.dart index 96fb40f3..3a504e25 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_state.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/one_time_order/one_time_order_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; + import '../../utils/time_parsing_utils.dart'; enum OneTimeOrderStatus { initial, loading, success, failure } @@ -162,9 +163,8 @@ class OneTimeOrderState extends Equatable { buffer.write('$minutes min'); } - if (first.lunchBreak != null && - first.lunchBreak != 'NO_BREAK' && - first.lunchBreak!.isNotEmpty) { + if (first.lunchBreak != 'NO_BREAK' && + first.lunchBreak.isNotEmpty) { buffer.write(' (${first.lunchBreak} break)'); } diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_state.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_state.dart index 229ff05d..c024994b 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_state.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/permanent_order/permanent_order_state.dart @@ -148,8 +148,8 @@ class PermanentOrderState extends Equatable { sum + (p.count * roleCostById(p.role)), ); - /// Estimated total cost: sum of (count * costPerHour * hours) per position. - double get estimatedTotal { + /// Daily cost: sum of (count * costPerHour * hours) per position. + double get dailyCost { double total = 0; for (final PermanentOrderPosition p in positions) { final double hours = parseHoursFromTimes(p.startTime, p.endTime); @@ -158,6 +158,12 @@ class PermanentOrderState extends Equatable { 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"). String get formattedRepeatDays => permanentDays.map( (String day) => day[0] + day.substring(1).toLowerCase(), diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_state.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_state.dart index eaa5d0b4..522a9c35 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_state.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/blocs/recurring_order/recurring_order_state.dart @@ -1,5 +1,6 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../../utils/schedule_utils.dart'; import '../../utils/time_parsing_utils.dart'; enum RecurringOrderStatus { initial, loading, success, failure } @@ -155,8 +156,8 @@ class RecurringOrderState extends Equatable { sum + (p.count * roleCostById(p.role)), ); - /// Estimated total cost: sum of (count * costPerHour * hours) per position. - double get estimatedTotal { + /// Daily cost: sum of (count * costPerHour * hours) per position. + double get dailyCost { double total = 0; for (final RecurringOrderPosition p in positions) { final double hours = parseHoursFromTimes(p.startTime, p.endTime); @@ -165,6 +166,31 @@ class RecurringOrderState extends Equatable { 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 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"). String get formattedRepeatDays => recurringDays.map( (String day) => day[0] + day.substring(1).toLowerCase(), diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/models/review_order_arguments.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/models/review_order_arguments.dart index f833ca8b..c00a1e78 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/models/review_order_arguments.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/models/review_order_arguments.dart @@ -25,6 +25,7 @@ class ReviewOrderArguments { this.scheduleStartDate, this.scheduleEndDate, this.scheduleRepeatDays, + this.totalLabel, }); final ReviewOrderType orderType; @@ -45,4 +46,7 @@ class ReviewOrderArguments { final String? scheduleStartDate; final String? scheduleEndDate; final String? scheduleRepeatDays; + + /// Optional label override for the total banner (e.g. "Estimated Weekly Total"). + final String? totalLabel; } 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 d1220dd2..c15dcdb6 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 @@ -1,3 +1,4 @@ +import 'package:core_localization/core_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_modular/flutter_modular.dart'; @@ -143,6 +144,7 @@ class PermanentOrderPage extends StatelessWidget { estimatedTotal: state.estimatedTotal, scheduleStartDate: DateFormat.yMMMd().format(state.startDate), scheduleRepeatDays: state.formattedRepeatDays, + totalLabel: t.client_create_order.review.estimated_weekly_total, ), ); diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/review_order_page.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/review_order_page.dart index 44500629..c92ef85d 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/review_order_page.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/pages/review_order_page.dart @@ -52,6 +52,7 @@ class ReviewOrderPage extends StatelessWidget { totalWorkers: args.totalWorkers, totalCostPerHour: args.totalCostPerHour, estimatedTotal: args.estimatedTotal, + totalLabel: args.totalLabel, showEditButtons: showEdit, onEditBasics: showEdit ? () => Modular.to.popSafe() : null, onEditSchedule: showEdit ? () => Modular.to.popSafe() : null, diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_total_banner.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_total_banner.dart index 93a88392..82430fd9 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_total_banner.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_total_banner.dart @@ -5,14 +5,20 @@ import 'package:flutter/material.dart'; /// A highlighted banner displaying the estimated total cost. /// /// 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 { const ReviewOrderTotalBanner({ required this.totalAmount, + this.label, super.key, }); + /// The total monetary amount to display. final double totalAmount; + /// Optional label override. Defaults to the localized "Estimated Total". + final String? label; + @override Widget build(BuildContext context) { return Container( @@ -28,7 +34,7 @@ class ReviewOrderTotalBanner extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - t.client_create_order.review.estimated_total, + label ?? t.client_create_order.review.estimated_total, style: UiTypography.body2m, ), Text( diff --git a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_view.dart b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_view.dart index 3ab576cd..6e8ef48c 100644 --- a/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_view.dart +++ b/apps/mobile/packages/features/client/orders/create_order/lib/src/presentation/widgets/review_order/review_order_view.dart @@ -32,6 +32,7 @@ class ReviewOrderView extends StatelessWidget { this.onEditSchedule, this.onEditPositions, this.submitLabel, + this.totalLabel, this.isLoading = false, super.key, }); @@ -51,6 +52,10 @@ class ReviewOrderView extends StatelessWidget { final VoidCallback? onEditSchedule; final VoidCallback? onEditPositions; 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; @override @@ -92,7 +97,10 @@ class ReviewOrderView extends StatelessWidget { onEdit: showEditButtons ? onEditPositions : null, ), const SizedBox(height: UiConstants.space3), - ReviewOrderTotalBanner(totalAmount: estimatedTotal), + ReviewOrderTotalBanner( + totalAmount: estimatedTotal, + label: totalLabel, + ), const SizedBox(height: UiConstants.space4), ], ),