feat: Add estimated weekly total label and refactor cost calculations for one-time and recurring orders
This commit is contained in:
@@ -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": {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user