feat: Implement reorder functionality in ClientCreateOrderRepository and update related interfaces and use cases
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
class ReportsPage extends StatefulWidget {
|
||||
const ReportsPage({super.key});
|
||||
@@ -36,8 +37,8 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
DateTime.now(),
|
||||
),
|
||||
(
|
||||
DateTime(DateTime.now().year, ((DateTime.now().month - 1) ~/ 3) * 3 + 1,
|
||||
1),
|
||||
DateTime(
|
||||
DateTime.now().year, ((DateTime.now().month - 1) ~/ 3) * 3 + 1, 1),
|
||||
DateTime.now(),
|
||||
),
|
||||
];
|
||||
@@ -102,8 +103,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () =>
|
||||
Modular.to.navigate('/client-main/home'),
|
||||
onTap: () => Modular.to.toClientHome(),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -209,8 +209,8 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
}
|
||||
|
||||
final summary = (state as ReportsSummaryLoaded).summary;
|
||||
final currencyFmt =
|
||||
NumberFormat.currency(symbol: '\$', decimalDigits: 0);
|
||||
final currencyFmt = NumberFormat.currency(
|
||||
symbol: '\$', decimalDigits: 0);
|
||||
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
@@ -261,8 +261,7 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
icon: UiIcons.trendingUp,
|
||||
label: context
|
||||
.t.client_reports.metrics.fill_rate.label,
|
||||
value:
|
||||
'${summary.fillRate.toStringAsFixed(0)}%',
|
||||
value: '${summary.fillRate.toStringAsFixed(0)}%',
|
||||
badgeText: context
|
||||
.t.client_reports.metrics.fill_rate.badge,
|
||||
badgeColor: UiColors.tagInProgress,
|
||||
@@ -271,12 +270,12 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
),
|
||||
_MetricCard(
|
||||
icon: UiIcons.clock,
|
||||
label: context.t.client_reports.metrics
|
||||
.avg_fill_time.label,
|
||||
label: context
|
||||
.t.client_reports.metrics.avg_fill_time.label,
|
||||
value:
|
||||
'${summary.avgFillTimeHours.toStringAsFixed(1)} hrs',
|
||||
badgeText: context.t.client_reports.metrics
|
||||
.avg_fill_time.badge,
|
||||
badgeText: context
|
||||
.t.client_reports.metrics.avg_fill_time.badge,
|
||||
badgeColor: UiColors.tagInProgress,
|
||||
badgeTextColor: UiColors.textLink,
|
||||
iconColor: UiColors.iconActive,
|
||||
@@ -474,8 +473,7 @@ class _MetricCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
@@ -580,4 +578,3 @@ class _ReportCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
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/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/performance/performance_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/pages/coverage_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/no_show_report_page.dart';
|
||||
@@ -26,7 +24,6 @@ class ReportsModule extends Module {
|
||||
i.addLazySingleton<ReportsRepository>(ReportsRepositoryImpl.new);
|
||||
i.add<DailyOpsBloc>(DailyOpsBloc.new);
|
||||
i.add<SpendBloc>(SpendBloc.new);
|
||||
i.add<CoverageBloc>(CoverageBloc.new);
|
||||
i.add<ForecastBloc>(ForecastBloc.new);
|
||||
i.add<PerformanceBloc>(PerformanceBloc.new);
|
||||
i.add<NoShowBloc>(NoShowBloc.new);
|
||||
@@ -41,6 +38,5 @@ class ReportsModule extends Module {
|
||||
r.child('/forecast', child: (_) => const ForecastReportPage());
|
||||
r.child('/performance', child: (_) => const PerformanceReportPage());
|
||||
r.child('/no-show', child: (_) => const NoShowReportPage());
|
||||
r.child('/coverage', child: (_) => const CoverageReportPage());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user