feat: Implement reorder functionality in ClientCreateOrderRepository and update related interfaces and use cases

This commit is contained in:
Achintha Isuru
2026-02-19 16:14:43 -05:00
parent b85ea5fb7f
commit 889bf90e71
10 changed files with 37 additions and 514 deletions

View File

@@ -5,17 +5,14 @@ publish_to: none
resolution: workspace resolution: workspace
environment: environment:
sdk: '>=3.10.0 <4.0.0' sdk: ">=3.10.0 <4.0.0"
flutter: ">=3.0.0" flutter: ">=3.0.0"
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_bloc: ^8.1.0
flutter_modular: ^6.3.0 # Architecture Packages
equatable: ^2.0.5
# Architecture Packages
design_system: design_system:
path: ../../../design_system path: ../../../design_system
core_localization: core_localization:
@@ -30,10 +27,12 @@ dependencies:
path: ../view_orders path: ../view_orders
billing: billing:
path: ../billing path: ../billing
krow_core:
path: ../../../core
# Intentionally commenting these out as they might not exist yet flutter_bloc: ^8.1.0
# client_settings: flutter_modular: ^6.3.0
# path: ../settings equatable: ^2.0.5
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -390,6 +390,12 @@ class ClientCreateOrderRepositoryImpl implements ClientCreateOrderRepositoryInte
throw UnimplementedError('Rapid order IA is not connected yet.'); throw UnimplementedError('Rapid order IA is not connected yet.');
} }
@override
Future<void> reorder(String previousOrderId, DateTime newDate) async {
// TODO: Implement reorder functionality to fetch the previous order and create a new one with the updated date.
throw UnimplementedError('Reorder functionality is not yet implemented.');
}
double _calculateShiftCost(domain.OneTimeOrder order) { double _calculateShiftCost(domain.OneTimeOrder order) {
double total = 0; double total = 0;
for (final domain.OneTimeOrderPosition position in order.positions) { for (final domain.OneTimeOrderPosition position in order.positions) {

View File

@@ -27,4 +27,10 @@ abstract interface class ClientCreateOrderRepositoryInterface {
/// ///
/// [description] is the text message (or transcribed voice) describing the need. /// [description] is the text message (or transcribed voice) describing the need.
Future<void> createRapidOrder(String description); Future<void> createRapidOrder(String description);
/// Reorders an existing staffing order with a new date.
///
/// [previousOrderId] is the ID of the order to reorder.
/// [newDate] is the new date for the order.
Future<void> reorder(String previousOrderId, DateTime newDate);
} }

View File

@@ -13,7 +13,7 @@ class ReorderArguments {
} }
/// Use case for reordering an existing staffing order. /// Use case for reordering an existing staffing order.
class ReorderUseCase implements UseCase<Future<void>, ReorderArguments> { class ReorderUseCase implements UseCase<ReorderArguments, void> {
const ReorderUseCase(this._repository); const ReorderUseCase(this._repository);
final ClientCreateOrderRepositoryInterface _repository; final ClientCreateOrderRepositoryInterface _repository;

View File

@@ -85,7 +85,7 @@ class DashboardWidgetBuilder extends StatelessWidget {
return; return;
} }
Modular.to.navigate( Modular.to.navigate(
'/client-main/orders/', ClientPaths.orders,
arguments: <String, dynamic>{ arguments: <String, dynamic>{
'initialDate': initialDate.toIso8601String(), 'initialDate': initialDate.toIso8601String(),
}, },

View File

@@ -1,464 +0,0 @@
import 'package:client_reports/src/presentation/blocs/coverage/coverage_bloc.dart';
import 'package:client_reports/src/presentation/blocs/coverage/coverage_event.dart';
import 'package:client_reports/src/presentation/blocs/coverage/coverage_state.dart';
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
class CoverageReportPage extends StatefulWidget {
const CoverageReportPage({super.key});
@override
State<CoverageReportPage> createState() => _CoverageReportPageState();
}
class _CoverageReportPageState extends State<CoverageReportPage> {
DateTime _startDate = DateTime.now();
DateTime _endDate = DateTime.now().add(const Duration(days: 6));
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => Modular.get<CoverageBloc>()
..add(LoadCoverageReport(startDate: _startDate, endDate: _endDate)),
child: Scaffold(
backgroundColor: UiColors.bgMenu,
body: BlocBuilder<CoverageBloc, CoverageState>(
builder: (context, state) {
if (state is CoverageLoading) {
return const Center(child: CircularProgressIndicator());
}
if (state is CoverageError) {
return Center(child: Text(state.message));
}
if (state is CoverageLoaded) {
final report = state.report;
// Compute "Full" and "Needs Help" counts from daily coverage
final fullDays = report.dailyCoverage
.where((d) => d.percentage >= 100)
.length;
final needsHelpDays = report.dailyCoverage
.where((d) => d.percentage < 80)
.length;
return SingleChildScrollView(
child: Column(
children: [
// ── Header ───────────────────────────────────────────
Container(
padding: const EdgeInsets.only(
top: 60,
left: 20,
right: 20,
bottom: 80, // Increased bottom padding for overlap background
),
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [
UiColors.primary,
UiColors.buttonPrimaryHover,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
children: [
// Title row
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
GestureDetector(
onTap: () => Navigator.of(context).pop(),
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: UiColors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
UiIcons.arrowLeft,
color: UiColors.white,
size: 20,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
context.t.client_reports.coverage_report
.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: UiColors.white,
),
),
Text(
context.t.client_reports.coverage_report
.subtitle,
style: TextStyle(
fontSize: 12,
color:
UiColors.white.withOpacity(0.7),
),
),
],
),
],
),
// Export button
/*
GestureDetector(
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.t.client_reports.coverage_report
.placeholders.export_message,
),
duration: const Duration(seconds: 2),
),
);
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(8),
),
child: const Row(
children: [
Icon(UiIcons.download,
size: 14, color: UiColors.primary),
SizedBox(width: 6),
Text(
'Export',
style: TextStyle(
color: UiColors.primary,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
*/
],
),
],
),
),
// ── 3 summary stat chips (Moved here for overlap) ──
Transform.translate(
offset: const Offset(0, -60), // Pull up to overlap header
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
_CoverageStatCard(
icon: UiIcons.trendingUp,
label: context.t.client_reports.coverage_report.metrics.avg_coverage,
value: '${report.overallCoverage.toStringAsFixed(0)}%',
iconColor: UiColors.primary,
),
const SizedBox(width: 12),
_CoverageStatCard(
icon: UiIcons.checkCircle,
label: context.t.client_reports.coverage_report.metrics.full,
value: fullDays.toString(),
iconColor: UiColors.success,
),
const SizedBox(width: 12),
_CoverageStatCard(
icon: UiIcons.warning,
label: context.t.client_reports.coverage_report.metrics.needs_help,
value: needsHelpDays.toString(),
iconColor: UiColors.error,
),
],
),
),
),
// ── Content ──────────────────────────────────────────
Transform.translate(
offset: const Offset(0, -60), // Pull up to overlap header
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 32),
// Section label
Text(
context.t.client_reports.coverage_report.next_7_days,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: UiColors.textPrimary,
letterSpacing: 0.5,
),
),
const SizedBox(height: 16),
if (report.dailyCoverage.isEmpty)
Container(
padding: const EdgeInsets.all(40),
alignment: Alignment.center,
child: Text(
context.t.client_reports.coverage_report.empty_state,
style: const TextStyle(
color: UiColors.textSecondary,
),
),
)
else
...report.dailyCoverage.map(
(day) => _DayCoverageCard(
date: DateFormat('EEE, MMM d').format(day.date),
filled: day.filled,
needed: day.needed,
percentage: day.percentage,
),
),
const SizedBox(height: 100),
],
),
),
),
],
),
);
}
return const SizedBox.shrink();
},
),
),
);
}
}
// ── Header stat chip (inside the blue header) ─────────────────────────────────
// ── Header stat card (boxes inside the blue header overlap) ───────────────────
class _CoverageStatCard extends StatelessWidget {
final IconData icon;
final String label;
final String value;
final Color iconColor;
const _CoverageStatCard({
required this.icon,
required this.label,
required this.value,
required this.iconColor,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
padding: const EdgeInsets.all(16), // Increased padding
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(16), // More rounded
boxShadow: [
BoxShadow(
color: UiColors.black.withOpacity(0.04),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
icon,
size: 14,
color: iconColor,
),
const SizedBox(width: 6),
Expanded(
child: Text(
label,
style: const TextStyle(
fontSize: 11,
color: UiColors.textSecondary,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 20, // Slightly smaller to fit if needed
fontWeight: FontWeight.bold,
color: UiColors.textPrimary,
),
),
],
),
),
);
}
}
// ── Day coverage card ─────────────────────────────────────────────────────────
class _DayCoverageCard extends StatelessWidget {
final String date;
final int filled;
final int needed;
final double percentage;
const _DayCoverageCard({
required this.date,
required this.filled,
required this.needed,
required this.percentage,
});
@override
Widget build(BuildContext context) {
final isFullyStaffed = percentage >= 100;
final spotsRemaining = (needed - filled).clamp(0, needed);
final barColor = percentage >= 95
? UiColors.success
: percentage >= 80
? UiColors.primary
: UiColors.error;
final badgeColor = percentage >= 95
? UiColors.success
: percentage >= 80
? UiColors.primary
: UiColors.error;
final badgeBg = percentage >= 95
? UiColors.tagSuccess
: percentage >= 80
? UiColors.primary.withOpacity(0.1) // Blue tint
: UiColors.tagError;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: UiColors.black.withOpacity(0.03),
blurRadius: 6,
),
],
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
date,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: UiColors.textPrimary,
),
),
const SizedBox(height: 2),
Text(
context.t.client_reports.coverage_report.shift_item.confirmed_workers(confirmed: filled.toString(), needed: needed.toString()),
style: const TextStyle(
fontSize: 12,
color: UiColors.textSecondary,
),
),
],
),
// Percentage badge
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: badgeBg,
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${percentage.toStringAsFixed(0)}%',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: badgeColor,
),
),
),
],
),
const SizedBox(height: 12),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: (percentage / 100).clamp(0.0, 1.0),
backgroundColor: UiColors.bgSecondary,
valueColor: AlwaysStoppedAnimation<Color>(barColor),
minHeight: 6,
),
),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: Text(
isFullyStaffed
? context.t.client_reports.coverage_report.shift_item.fully_staffed
: spotsRemaining == 1
? context.t.client_reports.coverage_report.shift_item.one_spot_remaining
: context.t.client_reports.coverage_report.shift_item.spots_remaining(count: spotsRemaining.toString()),
style: TextStyle(
fontSize: 11,
color: isFullyStaffed
? UiColors.success
: UiColors.textSecondary,
fontWeight: isFullyStaffed
? FontWeight.w500
: FontWeight.normal,
),
),
),
],
),
);
}
}

View File

@@ -7,6 +7,7 @@ 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';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
class ReportsPage extends StatefulWidget { class ReportsPage extends StatefulWidget {
const ReportsPage({super.key}); const ReportsPage({super.key});
@@ -36,8 +37,8 @@ class _ReportsPageState extends State<ReportsPage>
DateTime.now(), DateTime.now(),
), ),
( (
DateTime(DateTime.now().year, ((DateTime.now().month - 1) ~/ 3) * 3 + 1, DateTime(
1), DateTime.now().year, ((DateTime.now().month - 1) ~/ 3) * 3 + 1, 1),
DateTime.now(), DateTime.now(),
), ),
]; ];
@@ -102,8 +103,7 @@ class _ReportsPageState extends State<ReportsPage>
Row( Row(
children: [ children: [
GestureDetector( GestureDetector(
onTap: () => onTap: () => Modular.to.toClientHome(),
Modular.to.navigate('/client-main/home'),
child: Container( child: Container(
width: 40, width: 40,
height: 40, height: 40,
@@ -209,8 +209,8 @@ class _ReportsPageState extends State<ReportsPage>
} }
final summary = (state as ReportsSummaryLoaded).summary; final summary = (state as ReportsSummaryLoaded).summary;
final currencyFmt = final currencyFmt = NumberFormat.currency(
NumberFormat.currency(symbol: '\$', decimalDigits: 0); symbol: '\$', decimalDigits: 0);
return GridView.count( return GridView.count(
crossAxisCount: 2, crossAxisCount: 2,
@@ -261,8 +261,7 @@ class _ReportsPageState extends State<ReportsPage>
icon: UiIcons.trendingUp, icon: UiIcons.trendingUp,
label: context label: context
.t.client_reports.metrics.fill_rate.label, .t.client_reports.metrics.fill_rate.label,
value: value: '${summary.fillRate.toStringAsFixed(0)}%',
'${summary.fillRate.toStringAsFixed(0)}%',
badgeText: context badgeText: context
.t.client_reports.metrics.fill_rate.badge, .t.client_reports.metrics.fill_rate.badge,
badgeColor: UiColors.tagInProgress, badgeColor: UiColors.tagInProgress,
@@ -271,12 +270,12 @@ class _ReportsPageState extends State<ReportsPage>
), ),
_MetricCard( _MetricCard(
icon: UiIcons.clock, icon: UiIcons.clock,
label: context.t.client_reports.metrics label: context
.avg_fill_time.label, .t.client_reports.metrics.avg_fill_time.label,
value: value:
'${summary.avgFillTimeHours.toStringAsFixed(1)} hrs', '${summary.avgFillTimeHours.toStringAsFixed(1)} hrs',
badgeText: context.t.client_reports.metrics badgeText: context
.avg_fill_time.badge, .t.client_reports.metrics.avg_fill_time.badge,
badgeColor: UiColors.tagInProgress, badgeColor: UiColors.tagInProgress,
badgeTextColor: UiColors.textLink, badgeTextColor: UiColors.textLink,
iconColor: UiColors.iconActive, iconColor: UiColors.iconActive,
@@ -474,8 +473,7 @@ class _MetricCard extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Container( Container(
padding: padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: badgeColor, color: badgeColor,
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
@@ -580,4 +578,3 @@ class _ReportCard extends StatelessWidget {
); );
} }
} }

View File

@@ -1,13 +1,11 @@
import 'package:client_reports/src/data/repositories_impl/reports_repository_impl.dart'; import 'package:client_reports/src/data/repositories_impl/reports_repository_impl.dart';
import 'package:client_reports/src/domain/repositories/reports_repository.dart'; import 'package:client_reports/src/domain/repositories/reports_repository.dart';
import 'package:client_reports/src/presentation/blocs/daily_ops/daily_ops_bloc.dart'; import 'package:client_reports/src/presentation/blocs/daily_ops/daily_ops_bloc.dart';
import 'package:client_reports/src/presentation/blocs/spend/spend_bloc.dart';
import 'package:client_reports/src/presentation/blocs/coverage/coverage_bloc.dart';
import 'package:client_reports/src/presentation/blocs/forecast/forecast_bloc.dart'; import 'package:client_reports/src/presentation/blocs/forecast/forecast_bloc.dart';
import 'package:client_reports/src/presentation/blocs/performance/performance_bloc.dart';
import 'package:client_reports/src/presentation/blocs/no_show/no_show_bloc.dart'; import 'package:client_reports/src/presentation/blocs/no_show/no_show_bloc.dart';
import 'package:client_reports/src/presentation/blocs/performance/performance_bloc.dart';
import 'package:client_reports/src/presentation/blocs/spend/spend_bloc.dart';
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart'; import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart';
import 'package:client_reports/src/presentation/pages/coverage_report_page.dart';
import 'package:client_reports/src/presentation/pages/daily_ops_report_page.dart'; import 'package:client_reports/src/presentation/pages/daily_ops_report_page.dart';
import 'package:client_reports/src/presentation/pages/forecast_report_page.dart'; import 'package:client_reports/src/presentation/pages/forecast_report_page.dart';
import 'package:client_reports/src/presentation/pages/no_show_report_page.dart'; import 'package:client_reports/src/presentation/pages/no_show_report_page.dart';
@@ -26,7 +24,6 @@ class ReportsModule extends Module {
i.addLazySingleton<ReportsRepository>(ReportsRepositoryImpl.new); i.addLazySingleton<ReportsRepository>(ReportsRepositoryImpl.new);
i.add<DailyOpsBloc>(DailyOpsBloc.new); i.add<DailyOpsBloc>(DailyOpsBloc.new);
i.add<SpendBloc>(SpendBloc.new); i.add<SpendBloc>(SpendBloc.new);
i.add<CoverageBloc>(CoverageBloc.new);
i.add<ForecastBloc>(ForecastBloc.new); i.add<ForecastBloc>(ForecastBloc.new);
i.add<PerformanceBloc>(PerformanceBloc.new); i.add<PerformanceBloc>(PerformanceBloc.new);
i.add<NoShowBloc>(NoShowBloc.new); i.add<NoShowBloc>(NoShowBloc.new);
@@ -41,6 +38,5 @@ class ReportsModule extends Module {
r.child('/forecast', child: (_) => const ForecastReportPage()); r.child('/forecast', child: (_) => const ForecastReportPage());
r.child('/performance', child: (_) => const PerformanceReportPage()); r.child('/performance', child: (_) => const PerformanceReportPage());
r.child('/no-show', child: (_) => const NoShowReportPage()); r.child('/no-show', child: (_) => const NoShowReportPage());
r.child('/coverage', child: (_) => const CoverageReportPage());
} }
} }

View File

@@ -3,7 +3,6 @@ import 'package:design_system/design_system.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';
import 'package:krow_core/core.dart';
import '../../blocs/client_settings_bloc.dart'; import '../../blocs/client_settings_bloc.dart';
/// A widget that displays the log out button. /// A widget that displays the log out button.
@@ -59,7 +58,7 @@ class SettingsLogout extends StatelessWidget {
style: UiTypography.headline3m.textPrimary, style: UiTypography.headline3m.textPrimary,
), ),
content: Text( content: Text(
t.client_settings.profile.log_out_confirmation, 'Are you sure you want to log out?',
style: UiTypography.body2r.textSecondary, style: UiTypography.body2r.textSecondary,
), ),
actions: <Widget>[ actions: <Widget>[

View File

@@ -1,16 +0,0 @@
abstract class StaffMainRoutes {
static const String modulePath = '/worker-main';
static const String shifts = '/shifts';
static const String payments = '/payments';
static const String home = '/home';
static const String clockIn = '/clock-in';
static const String profile = '/profile';
// Full paths
static const String shiftsFull = '$modulePath$shifts';
static const String paymentsFull = '$modulePath$payments';
static const String homeFull = '$modulePath$home';
static const String clockInFull = '$modulePath$clockIn';
static const String profileFull = '$modulePath$profile';
}