feat: Refactor Reports page components and implement new metric and report card widgets
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
library client_reports;
|
library;
|
||||||
|
|
||||||
export 'src/reports_module.dart';
|
export 'src/reports_module.dart';
|
||||||
export 'src/presentation/pages/reports_page.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_bloc.dart';
|
||||||
import 'package:client_reports/src/presentation/blocs/summary/reports_summary_event.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: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: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 {
|
class ReportsPage extends StatefulWidget {
|
||||||
const ReportsPage({super.key});
|
const ReportsPage({super.key});
|
||||||
|
|
||||||
@@ -49,7 +51,11 @@ class _ReportsPageState extends State<ReportsPage>
|
|||||||
_tabController = TabController(length: 4, vsync: this);
|
_tabController = TabController(length: 4, vsync: this);
|
||||||
_summaryBloc = Modular.get<ReportsSummaryBloc>();
|
_summaryBloc = Modular.get<ReportsSummaryBloc>();
|
||||||
_loadSummary(0);
|
_loadSummary(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
_tabController.addListener(() {
|
_tabController.addListener(() {
|
||||||
if (!_tabController.indexIsChanging) {
|
if (!_tabController.indexIsChanging) {
|
||||||
_loadSummary(_tabController.index);
|
_loadSummary(_tabController.index);
|
||||||
@@ -80,88 +86,10 @@ class _ReportsPageState extends State<ReportsPage>
|
|||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Header
|
// Header with title and tabs
|
||||||
Container(
|
ReportsHeader(
|
||||||
padding: const EdgeInsets.only(
|
tabController: _tabController,
|
||||||
top: 60,
|
onTabChanged: _loadSummary,
|
||||||
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: [
|
|
||||||
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),
|
|
||||||
// Tabs
|
|
||||||
Container(
|
|
||||||
height: 44,
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.white.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: TabBar(
|
|
||||||
controller: _tabController,
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
@@ -170,228 +98,13 @@ class _ReportsPageState extends State<ReportsPage>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Key Metrics — driven by BLoC
|
// Key Metrics Grid
|
||||||
BlocBuilder<ReportsSummaryBloc, ReportsSummaryState>(
|
const MetricsGrid(),
|
||||||
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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Quick Reports
|
// Quick Reports Section
|
||||||
Row(
|
const QuickReportsSection(),
|
||||||
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),
|
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
@@ -404,177 +117,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