diff --git a/apps/mobile/packages/features/client/reports/lib/client_reports.dart b/apps/mobile/packages/features/client/reports/lib/client_reports.dart index 1ea6bd62..c8201546 100644 --- a/apps/mobile/packages/features/client/reports/lib/client_reports.dart +++ b/apps/mobile/packages/features/client/reports/lib/client_reports.dart @@ -1,4 +1,4 @@ -library client_reports; +library; export 'src/reports_module.dart'; export 'src/presentation/pages/reports_page.dart'; diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/reports_page.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/reports_page.dart index fbc60def..823d163b 100644 --- a/apps/mobile/packages/features/client/reports/lib/src/presentation/pages/reports_page.dart +++ b/apps/mobile/packages/features/client/reports/lib/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 _tabController = TabController(length: 4, vsync: this); _summaryBloc = Modular.get(); _loadSummary(0); + } + @override + void didChangeDependencies() { + super.didChangeDependencies(); _tabController.addListener(() { if (!_tabController.indexIsChanging) { _loadSummary(_tabController.index); @@ -80,88 +86,10 @@ class _ReportsPageState extends State body: SingleChildScrollView( child: Column( children: [ - // Header - 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: [ - 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), - ], - ), - ), - ], - ), + // Header with title and tabs + ReportsHeader( + tabController: _tabController, + onTabChanged: _loadSummary, ), // Content @@ -170,228 +98,13 @@ class _ReportsPageState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Key Metrics — driven by BLoC - BlocBuilder( - 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 +117,3 @@ class _ReportsPageState extends State ); } } - -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, - ), - ), - ], - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/index.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/index.dart new file mode 100644 index 00000000..58d67814 --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/index.dart @@ -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'; diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metric_card.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metric_card.dart new file mode 100644 index 00000000..c1be6744 --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metric_card.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, + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid.dart new file mode 100644 index 00000000..6ebf44ce --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/metrics_grid.dart @@ -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( + 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, + ), + ], + ); + }, + ); + } +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/quick_reports_section.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/quick_reports_section.dart new file mode 100644 index 00000000..88219692 --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/quick_reports_section.dart @@ -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', + ), + ], + ), + ], + ); + } +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/report_card.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/report_card.dart new file mode 100644 index 00000000..d04bd137 --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/report_card.dart @@ -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, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/reports_header.dart b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/reports_header.dart new file mode 100644 index 00000000..9d4eaa34 --- /dev/null +++ b/apps/mobile/packages/features/client/reports/lib/src/presentation/widgets/reports_page/reports_header.dart @@ -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), + ], + ), + ); + } +}