Merge branch 'github-action' of https://github.com/Oloodi/krow-workforce into github-action
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
library client_reports;
|
||||
library;
|
||||
|
||||
export 'src/reports_module.dart';
|
||||
export 'src/presentation/pages/reports_page.dart';
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart';
|
||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_event.dart';
|
||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_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';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
import '../widgets/reports_page/index.dart';
|
||||
|
||||
/// The main Reports page for the client application.
|
||||
///
|
||||
/// Displays key performance metrics and quick access to various reports.
|
||||
/// Handles tab-based time period selection (Today, Week, Month, Quarter).
|
||||
class ReportsPage extends StatefulWidget {
|
||||
const ReportsPage({super.key});
|
||||
|
||||
@@ -49,7 +51,11 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
_tabController = TabController(length: 4, vsync: this);
|
||||
_summaryBloc = Modular.get<ReportsSummaryBloc>();
|
||||
_loadSummary(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_tabController.addListener(() {
|
||||
if (!_tabController.indexIsChanging) {
|
||||
_loadSummary(_tabController.index);
|
||||
@@ -170,228 +176,13 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Key Metrics — driven by BLoC
|
||||
BlocBuilder<ReportsSummaryBloc, ReportsSummaryState>(
|
||||
builder: (context, state) {
|
||||
if (state is ReportsSummaryLoading ||
|
||||
state is ReportsSummaryInitial) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 32),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (state is ReportsSummaryError) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagError,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(UiIcons.warning,
|
||||
color: UiColors.error, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.message,
|
||||
style: const TextStyle(
|
||||
color: UiColors.error, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final summary = (state as ReportsSummaryLoaded).summary;
|
||||
final currencyFmt = NumberFormat.currency(
|
||||
symbol: '\$', decimalDigits: 0);
|
||||
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.2,
|
||||
children: [
|
||||
_MetricCard(
|
||||
icon: UiIcons.clock,
|
||||
label: context
|
||||
.t.client_reports.metrics.total_hrs.label,
|
||||
value: summary.totalHours >= 1000
|
||||
? '${(summary.totalHours / 1000).toStringAsFixed(1)}k'
|
||||
: summary.totalHours.toStringAsFixed(0),
|
||||
badgeText: context
|
||||
.t.client_reports.metrics.total_hrs.badge,
|
||||
badgeColor: UiColors.tagRefunded,
|
||||
badgeTextColor: UiColors.primary,
|
||||
iconColor: UiColors.primary,
|
||||
),
|
||||
_MetricCard(
|
||||
icon: UiIcons.trendingUp,
|
||||
label: context
|
||||
.t.client_reports.metrics.ot_hours.label,
|
||||
value: summary.otHours.toStringAsFixed(0),
|
||||
badgeText: context
|
||||
.t.client_reports.metrics.ot_hours.badge,
|
||||
badgeColor: UiColors.tagValue,
|
||||
badgeTextColor: UiColors.textSecondary,
|
||||
iconColor: UiColors.textWarning,
|
||||
),
|
||||
_MetricCard(
|
||||
icon: UiIcons.dollar,
|
||||
label: context
|
||||
.t.client_reports.metrics.total_spend.label,
|
||||
value: summary.totalSpend >= 1000
|
||||
? '\$${(summary.totalSpend / 1000).toStringAsFixed(1)}k'
|
||||
: currencyFmt.format(summary.totalSpend),
|
||||
badgeText: context
|
||||
.t.client_reports.metrics.total_spend.badge,
|
||||
badgeColor: UiColors.tagSuccess,
|
||||
badgeTextColor: UiColors.textSuccess,
|
||||
iconColor: UiColors.success,
|
||||
),
|
||||
_MetricCard(
|
||||
icon: UiIcons.trendingUp,
|
||||
label: context
|
||||
.t.client_reports.metrics.fill_rate.label,
|
||||
value: '${summary.fillRate.toStringAsFixed(0)}%',
|
||||
badgeText: context
|
||||
.t.client_reports.metrics.fill_rate.badge,
|
||||
badgeColor: UiColors.tagInProgress,
|
||||
badgeTextColor: UiColors.textLink,
|
||||
iconColor: UiColors.iconActive,
|
||||
),
|
||||
_MetricCard(
|
||||
icon: UiIcons.clock,
|
||||
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,
|
||||
badgeColor: UiColors.tagInProgress,
|
||||
badgeTextColor: UiColors.textLink,
|
||||
iconColor: UiColors.iconActive,
|
||||
),
|
||||
_MetricCard(
|
||||
icon: UiIcons.warning,
|
||||
label: context
|
||||
.t.client_reports.metrics.no_show_rate.label,
|
||||
value:
|
||||
'${summary.noShowRate.toStringAsFixed(1)}%',
|
||||
badgeText: context
|
||||
.t.client_reports.metrics.no_show_rate.badge,
|
||||
badgeColor: summary.noShowRate < 5
|
||||
? UiColors.tagSuccess
|
||||
: UiColors.tagError,
|
||||
badgeTextColor: summary.noShowRate < 5
|
||||
? UiColors.textSuccess
|
||||
: UiColors.error,
|
||||
iconColor: UiColors.destructive,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
// Key Metrics Grid
|
||||
const MetricsGrid(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Quick Reports
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
context.t.client_reports.quick_reports.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
/*
|
||||
TextButton.icon(
|
||||
onPressed: () {},
|
||||
icon: const Icon(UiIcons.download, size: 16),
|
||||
label: Text(
|
||||
context.t.client_reports.quick_reports.export_all),
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.textLink,
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
*/
|
||||
],
|
||||
),
|
||||
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.3,
|
||||
children: [
|
||||
_ReportCard(
|
||||
icon: UiIcons.calendar,
|
||||
name: context
|
||||
.t.client_reports.quick_reports.cards.daily_ops,
|
||||
iconBgColor: UiColors.tagInProgress,
|
||||
iconColor: UiColors.primary,
|
||||
route: './daily-ops',
|
||||
),
|
||||
_ReportCard(
|
||||
icon: UiIcons.dollar,
|
||||
name: context
|
||||
.t.client_reports.quick_reports.cards.spend,
|
||||
iconBgColor: UiColors.tagSuccess,
|
||||
iconColor: UiColors.success,
|
||||
route: './spend',
|
||||
),
|
||||
_ReportCard(
|
||||
icon: UiIcons.users,
|
||||
name: context
|
||||
.t.client_reports.quick_reports.cards.coverage,
|
||||
iconBgColor: UiColors.tagInProgress,
|
||||
iconColor: UiColors.primary,
|
||||
route: './coverage',
|
||||
),
|
||||
_ReportCard(
|
||||
icon: UiIcons.warning,
|
||||
name: context
|
||||
.t.client_reports.quick_reports.cards.no_show,
|
||||
iconBgColor: UiColors.tagError,
|
||||
iconColor: UiColors.destructive,
|
||||
route: './no-show',
|
||||
),
|
||||
_ReportCard(
|
||||
icon: UiIcons.trendingUp,
|
||||
name: context
|
||||
.t.client_reports.quick_reports.cards.forecast,
|
||||
iconBgColor: UiColors.tagPending,
|
||||
iconColor: UiColors.textWarning,
|
||||
route: './forecast',
|
||||
),
|
||||
_ReportCard(
|
||||
icon: UiIcons.chart,
|
||||
name: context
|
||||
.t.client_reports.quick_reports.cards.performance,
|
||||
iconBgColor: UiColors.tagInProgress,
|
||||
iconColor: UiColors.primary,
|
||||
route: './performance',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
// Quick Reports Section
|
||||
const QuickReportsSection(),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
@@ -404,177 +195,3 @@ class _ReportsPageState extends State<ReportsPage>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MetricCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String value;
|
||||
final String badgeText;
|
||||
final Color badgeColor;
|
||||
final Color badgeTextColor;
|
||||
final Color iconColor;
|
||||
|
||||
const _MetricCard({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.badgeText,
|
||||
required this.badgeColor,
|
||||
required this.badgeTextColor,
|
||||
required this.iconColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.06),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
badgeText,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: badgeTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ReportCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String name;
|
||||
final Color iconBgColor;
|
||||
final Color iconColor;
|
||||
final String route;
|
||||
|
||||
const _ReportCard({
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.iconBgColor,
|
||||
required this.iconColor,
|
||||
required this.route,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => Modular.to.pushNamed(route),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.02),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: iconBgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, size: 20, color: iconColor),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
UiIcons.download,
|
||||
size: 12,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
context.t.client_reports.quick_reports.two_click_export,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export 'metric_card.dart';
|
||||
export 'metrics_grid.dart';
|
||||
export 'quick_reports_section.dart';
|
||||
export 'report_card.dart';
|
||||
export 'reports_header.dart';
|
||||
@@ -0,0 +1,112 @@
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A metric card widget for displaying key performance indicators.
|
||||
///
|
||||
/// Shows a metric with an icon, label, value, and a badge with contextual
|
||||
/// information. Used in the metrics grid of the reports page.
|
||||
class MetricCard extends StatelessWidget {
|
||||
/// The icon to display for this metric.
|
||||
final IconData icon;
|
||||
|
||||
/// The label describing the metric.
|
||||
final String label;
|
||||
|
||||
/// The main value to display (e.g., "1.2k", "$50,000").
|
||||
final String value;
|
||||
|
||||
/// Text to display in the badge.
|
||||
final String badgeText;
|
||||
|
||||
/// Background color for the badge.
|
||||
final Color badgeColor;
|
||||
|
||||
/// Text color for the badge.
|
||||
final Color badgeTextColor;
|
||||
|
||||
/// Color for the icon.
|
||||
final Color iconColor;
|
||||
|
||||
const MetricCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.badgeText,
|
||||
required this.badgeColor,
|
||||
required this.badgeTextColor,
|
||||
required this.iconColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.06),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Icon and Label
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Value and Badge
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Text(
|
||||
badgeText,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: badgeTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_bloc.dart';
|
||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_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:intl/intl.dart';
|
||||
|
||||
import 'metric_card.dart';
|
||||
|
||||
/// A grid of key metrics driven by the ReportsSummaryBloc.
|
||||
///
|
||||
/// Displays 6 metrics in a 2-column grid:
|
||||
/// - Total Hours
|
||||
/// - OT Hours
|
||||
/// - Total Spend
|
||||
/// - Fill Rate
|
||||
/// - Average Fill Time
|
||||
/// - No-Show Rate
|
||||
///
|
||||
/// Handles loading, error, and success states.
|
||||
class MetricsGrid extends StatelessWidget {
|
||||
const MetricsGrid({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ReportsSummaryBloc, ReportsSummaryState>(
|
||||
builder: (context, state) {
|
||||
// Loading or Initial State
|
||||
if (state is ReportsSummaryLoading || state is ReportsSummaryInitial) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 32),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
// Error State
|
||||
if (state is ReportsSummaryError) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.tagError,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(UiIcons.warning,
|
||||
color: UiColors.error, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.message,
|
||||
style: const TextStyle(
|
||||
color: UiColors.error, fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Loaded State
|
||||
final summary = (state as ReportsSummaryLoaded).summary;
|
||||
final currencyFmt = NumberFormat.currency(
|
||||
symbol: '\$', decimalDigits: 0);
|
||||
|
||||
return GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.2,
|
||||
children: [
|
||||
// Total Hours
|
||||
MetricCard(
|
||||
icon: UiIcons.clock,
|
||||
label: context.t.client_reports.metrics.total_hrs.label,
|
||||
value: summary.totalHours >= 1000
|
||||
? '${(summary.totalHours / 1000).toStringAsFixed(1)}k'
|
||||
: summary.totalHours.toStringAsFixed(0),
|
||||
badgeText: context.t.client_reports.metrics.total_hrs.badge,
|
||||
badgeColor: UiColors.tagRefunded,
|
||||
badgeTextColor: UiColors.primary,
|
||||
iconColor: UiColors.primary,
|
||||
),
|
||||
// OT Hours
|
||||
MetricCard(
|
||||
icon: UiIcons.trendingUp,
|
||||
label: context.t.client_reports.metrics.ot_hours.label,
|
||||
value: summary.otHours.toStringAsFixed(0),
|
||||
badgeText: context.t.client_reports.metrics.ot_hours.badge,
|
||||
badgeColor: UiColors.tagValue,
|
||||
badgeTextColor: UiColors.textSecondary,
|
||||
iconColor: UiColors.textWarning,
|
||||
),
|
||||
// Total Spend
|
||||
MetricCard(
|
||||
icon: UiIcons.dollar,
|
||||
label: context.t.client_reports.metrics.total_spend.label,
|
||||
value: summary.totalSpend >= 1000
|
||||
? '\$${(summary.totalSpend / 1000).toStringAsFixed(1)}k'
|
||||
: currencyFmt.format(summary.totalSpend),
|
||||
badgeText: context.t.client_reports.metrics.total_spend.badge,
|
||||
badgeColor: UiColors.tagSuccess,
|
||||
badgeTextColor: UiColors.textSuccess,
|
||||
iconColor: UiColors.success,
|
||||
),
|
||||
// Fill Rate
|
||||
MetricCard(
|
||||
icon: UiIcons.trendingUp,
|
||||
label: context.t.client_reports.metrics.fill_rate.label,
|
||||
value: '${summary.fillRate.toStringAsFixed(0)}%',
|
||||
badgeText: context.t.client_reports.metrics.fill_rate.badge,
|
||||
badgeColor: UiColors.tagInProgress,
|
||||
badgeTextColor: UiColors.textLink,
|
||||
iconColor: UiColors.iconActive,
|
||||
),
|
||||
// Average Fill Time
|
||||
MetricCard(
|
||||
icon: UiIcons.clock,
|
||||
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,
|
||||
badgeColor: UiColors.tagInProgress,
|
||||
badgeTextColor: UiColors.textLink,
|
||||
iconColor: UiColors.iconActive,
|
||||
),
|
||||
// No-Show Rate
|
||||
MetricCard(
|
||||
icon: UiIcons.warning,
|
||||
label: context.t.client_reports.metrics.no_show_rate.label,
|
||||
value: '${summary.noShowRate.toStringAsFixed(1)}%',
|
||||
badgeText: context.t.client_reports.metrics.no_show_rate.badge,
|
||||
badgeColor: summary.noShowRate < 5
|
||||
? UiColors.tagSuccess
|
||||
: UiColors.tagError,
|
||||
badgeTextColor: summary.noShowRate < 5
|
||||
? UiColors.textSuccess
|
||||
: UiColors.error,
|
||||
iconColor: UiColors.destructive,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'report_card.dart';
|
||||
|
||||
/// A section displaying quick access report cards.
|
||||
///
|
||||
/// Shows 4 quick report cards for:
|
||||
/// - Daily Operations
|
||||
/// - Spend Analysis
|
||||
/// - No-Show Rates
|
||||
/// - Performance Reports
|
||||
class QuickReportsSection extends StatelessWidget {
|
||||
const QuickReportsSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title
|
||||
Text(
|
||||
context.t.client_reports.quick_reports.title,
|
||||
style: UiTypography.headline2m.textPrimary,
|
||||
),
|
||||
|
||||
// Quick Reports Grid
|
||||
GridView.count(
|
||||
crossAxisCount: 2,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1.3,
|
||||
children: [
|
||||
// Daily Operations
|
||||
ReportCard(
|
||||
icon: UiIcons.calendar,
|
||||
name: context.t.client_reports.quick_reports.cards.daily_ops,
|
||||
iconBgColor: UiColors.tagInProgress,
|
||||
iconColor: UiColors.primary,
|
||||
route: './daily-ops',
|
||||
),
|
||||
// Spend Analysis
|
||||
ReportCard(
|
||||
icon: UiIcons.dollar,
|
||||
name: context.t.client_reports.quick_reports.cards.spend,
|
||||
iconBgColor: UiColors.tagSuccess,
|
||||
iconColor: UiColors.success,
|
||||
route: './spend',
|
||||
),
|
||||
// No-Show Rates
|
||||
ReportCard(
|
||||
icon: UiIcons.warning,
|
||||
name: context.t.client_reports.quick_reports.cards.no_show,
|
||||
iconBgColor: UiColors.tagError,
|
||||
iconColor: UiColors.destructive,
|
||||
route: './no-show',
|
||||
),
|
||||
// Performance Reports
|
||||
ReportCard(
|
||||
icon: UiIcons.chart,
|
||||
name:
|
||||
context.t.client_reports.quick_reports.cards.performance,
|
||||
iconBgColor: UiColors.tagInProgress,
|
||||
iconColor: UiColors.primary,
|
||||
route: './performance',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
/// A quick report card widget for navigating to specific reports.
|
||||
///
|
||||
/// Displays an icon, name, and a quick navigation to a report page.
|
||||
/// Used in the quick reports grid of the reports page.
|
||||
class ReportCard extends StatelessWidget {
|
||||
/// The icon to display for this report.
|
||||
final IconData icon;
|
||||
|
||||
/// The name/title of the report.
|
||||
final String name;
|
||||
|
||||
/// Background color for the icon container.
|
||||
final Color iconBgColor;
|
||||
|
||||
/// Color for the icon.
|
||||
final Color iconColor;
|
||||
|
||||
/// Navigation route to the report page.
|
||||
final String route;
|
||||
|
||||
const ReportCard({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.name,
|
||||
required this.iconBgColor,
|
||||
required this.iconColor,
|
||||
required this.route,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => Modular.to.pushNamed(route),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.black.withOpacity(0.02),
|
||||
blurRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Icon Container
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: iconBgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, size: 20, color: iconColor),
|
||||
),
|
||||
// Name and Export Info
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
UiIcons.download,
|
||||
size: 12,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
context.t.client_reports.quick_reports
|
||||
.two_click_export,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
/// Header widget for the Reports page.
|
||||
///
|
||||
/// Displays the title, back button, and tab selector for different time periods
|
||||
/// (Today, Week, Month, Quarter).
|
||||
class ReportsHeader extends StatelessWidget {
|
||||
const ReportsHeader({
|
||||
super.key,
|
||||
required this.onTabChanged,
|
||||
required this.tabController,
|
||||
});
|
||||
|
||||
/// Called when a tab is selected.
|
||||
final Function(int) onTabChanged;
|
||||
|
||||
/// The current tab controller for managing tab state.
|
||||
final TabController tabController;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 60,
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 32,
|
||||
),
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
UiColors.primary,
|
||||
UiColors.buttonPrimaryHover,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Title and Back Button
|
||||
Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Modular.to.toClientHome(),
|
||||
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),
|
||||
Text(
|
||||
context.t.client_reports.title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Tab Bar
|
||||
_buildTabBar(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the styled tab bar for time period selection.
|
||||
Widget _buildTabBar(BuildContext context) {
|
||||
return Container(
|
||||
height: 44,
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: tabController,
|
||||
onTap: onTabChanged,
|
||||
indicator: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
labelColor: UiColors.primary,
|
||||
unselectedLabelColor: UiColors.white,
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: [
|
||||
Tab(text: context.t.client_reports.tabs.today),
|
||||
Tab(text: context.t.client_reports.tabs.week),
|
||||
Tab(text: context.t.client_reports.tabs.month),
|
||||
Tab(text: context.t.client_reports.tabs.quarter),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user