From 4e8e6af86ef314a5dc8c5c26c7a1c3c7beb3c1c4 Mon Sep 17 00:00:00 2001 From: moon <56061215+MgKyawLay@users.noreply.github.com> Date: Thu, 23 Apr 2026 10:59:34 +0630 Subject: [PATCH] analysis page remake --- devtools_options.yaml | 3 + .../manager/analysis_chart_metric.dart | 4 +- .../presentation/pages/analysis_page.dart | 875 ++++++++++++------ .../presentation/pages/settings_page.dart | 40 +- 4 files changed, 603 insertions(+), 319 deletions(-) create mode 100644 devtools_options.yaml diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/features/analysis/presentation/manager/analysis_chart_metric.dart b/lib/features/analysis/presentation/manager/analysis_chart_metric.dart index 9d122e4..d335a6c 100644 --- a/lib/features/analysis/presentation/manager/analysis_chart_metric.dart +++ b/lib/features/analysis/presentation/manager/analysis_chart_metric.dart @@ -2,7 +2,7 @@ enum AnalysisChartMetric { scans, pointsUsed } extension AnalysisChartMetricX on AnalysisChartMetric { String get label => switch (this) { - AnalysisChartMetric.scans => 'Scans', - AnalysisChartMetric.pointsUsed => 'Points Used', + AnalysisChartMetric.scans => 'Member scans', + AnalysisChartMetric.pointsUsed => 'Coins redeemed', }; } diff --git a/lib/features/analysis/presentation/pages/analysis_page.dart b/lib/features/analysis/presentation/pages/analysis_page.dart index 3fbb8b1..c5b567a 100644 --- a/lib/features/analysis/presentation/pages/analysis_page.dart +++ b/lib/features/analysis/presentation/pages/analysis_page.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import '../manager/analysis_range.dart'; + import '../manager/analysis_chart_metric.dart'; +import '../manager/analysis_range.dart'; import '../manager/analysis_ui_state.dart'; import '../manager/analysis_view_model.dart'; import '../widgets/analysis_line_chart.dart'; @@ -19,7 +20,7 @@ class AnalysisPage extends ConsumerWidget { return Scaffold( backgroundColor: theme.scaffoldBackgroundColor, appBar: AppBar( - title: const Text('Analysis'), + title: const Text('Cashier Analysis'), backgroundColor: theme.scaffoldBackgroundColor, surfaceTintColor: theme.scaffoldBackgroundColor, elevation: 0, @@ -29,7 +30,7 @@ class AnalysisPage extends ConsumerWidget { onRefresh: () async => ref.invalidate(analysisViewModelProvider), child: ListView( physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.fromLTRB(16, 12, 16, 24), + padding: const EdgeInsets.fromLTRB(16, 8, 16, 96), children: [ _RangeSelector( selected: rangePreset, @@ -37,17 +38,15 @@ class AnalysisPage extends ConsumerWidget { .read(analysisRangeNotifierProvider.notifier) .setRange(preset), ), - const SizedBox(height: 10), - _ChartMetricSelector( - selected: chartMetric, - onSelected: (metric) => ref - .read(analysisChartMetricNotifierProvider.notifier) - .setMetric(metric), - ), const SizedBox(height: 12), stateAsync.when( - data: (state) => - _AnalysisBody(state: state, chartMetric: chartMetric), + data: (state) => _AnalysisBody( + state: state, + chartMetric: chartMetric, + onMetricSelected: (metric) => ref + .read(analysisChartMetricNotifierProvider.notifier) + .setMetric(metric), + ), loading: () => const _LoadingBody(), error: (error, stackTrace) => _ErrorBody( onRetry: () => ref.invalidate(analysisViewModelProvider), @@ -69,6 +68,7 @@ class _RangeSelector extends StatelessWidget { @override Widget build(BuildContext context) { return SegmentedButton( + showSelectedIcon: false, segments: AnalysisRangePreset.values .map( (preset) => ButtonSegment(value: preset, label: Text(preset.label)), @@ -84,17 +84,20 @@ class _RangeSelector extends StatelessWidget { } class _AnalysisBody extends StatelessWidget { - const _AnalysisBody({required this.state, required this.chartMetric}); + const _AnalysisBody({ + required this.state, + required this.chartMetric, + required this.onMetricSelected, + }); final AnalysisUiState state; final AnalysisChartMetric chartMetric; + final ValueChanged onMetricSelected; @override Widget build(BuildContext context) { final summary = state.summary ?? AnalysisSummaryUiModel.empty; final series = state.series ?? const []; - final rangeLabel = state.rangeLabel ?? ''; - final values = series .map( (p) => switch (chartMetric) { @@ -103,44 +106,210 @@ class _AnalysisBody extends StatelessWidget { }, ) .toList(growable: false); - final totalValue = switch (chartMetric) { AnalysisChartMetric.scans => summary.scans, AnalysisChartMetric.pointsUsed => summary.pointsUsed, }; - final xLabels = series.map((p) => p.label).toList(growable: false); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + _ShiftSummaryCard(state: state), + const SizedBox(height: 12), + _CashierActionGrid(summary: summary, state: state), + const SizedBox(height: 12), _ChartCard( - title: chartMetric.label, - subtitle: rangeLabel, + selectedMetric: chartMetric, + onMetricSelected: onMetricSelected, totalValue: totalValue, - chart: AnalysisLineChart(values: values), - xLabels: xLabels, + rangeLabel: state.rangeLabel ?? '', + chart: AnalysisLineChart(values: values, height: 132), + xLabels: series.map((p) => p.label).toList(growable: false), ), const SizedBox(height: 12), - _InsightRow(state: state), - const SizedBox(height: 12), - _KpiGrid(summary: summary), + _OperationalNotes(state: state), ], ); } } +class _ShiftSummaryCard extends StatelessWidget { + const _ShiftSummaryCard({required this.state}); + + final AnalysisUiState state; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final summary = state.summary ?? AnalysisSummaryUiModel.empty; + final pointsPerScan = state.pointsPerScan ?? 0; + + return Container( + decoration: _cardDecoration(context), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Icon( + Icons.point_of_sale_rounded, + color: colorScheme.onPrimaryContainer, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Loyalty counter', + style: textTheme.labelLarge?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 2), + Text( + state.rangeLabel ?? 'Selected range', + style: textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + ), + ), + ], + ), + ), + _StatusPill( + label: '${summary.successRatePercent}% success', + icon: Icons.verified_rounded, + ), + ], + ), + const SizedBox(height: 18), + Row( + children: [ + Expanded( + child: _HeroMetric( + label: 'Member scans', + value: summary.scans.toString(), + icon: Icons.qr_code_scanner_rounded, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _HeroMetric( + label: 'Points redeemed', + value: summary.pointsUsed.toString(), + icon: Icons.redeem_rounded, + ), + ), + ], + ), + const SizedBox(height: 12), + _InlineStatBar( + items: [ + _InlineStatItem( + label: 'Active members', + value: summary.activeUsers.toString(), + ), + _InlineStatItem( + label: 'Coins/scan', + value: pointsPerScan.toStringAsFixed(1), + ), + _InlineStatItem( + label: 'Range users', + value: summary.users.toString(), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _CashierActionGrid extends StatelessWidget { + const _CashierActionGrid({required this.summary, required this.state}); + + final AnalysisSummaryUiModel summary; + final AnalysisUiState state; + + @override + Widget build(BuildContext context) { + final averageScansPerDay = state.averageScansPerDay ?? 0; + final averagePointsPerDay = state.averagePointsPerDay ?? 0; + final activeUserRatePercent = state.activeUserRatePercent ?? 0; + + return LayoutBuilder( + builder: (context, constraints) { + final tileWidth = (constraints.maxWidth - 10) / 2; + + return Wrap( + spacing: 10, + runSpacing: 10, + children: [ + _ActionMetricTile( + width: tileWidth, + title: 'Avg scans/day', + value: averageScansPerDay.toStringAsFixed(1), + icon: Icons.timeline_rounded, + ), + _ActionMetricTile( + width: tileWidth, + title: 'Avg coins/day', + value: averagePointsPerDay.toStringAsFixed(1), + icon: Icons.savings_rounded, + ), + _ActionMetricTile( + width: tileWidth, + title: 'Active rate', + value: '$activeUserRatePercent%', + icon: Icons.people_alt_rounded, + ), + _ActionMetricTile( + width: tileWidth, + title: 'Scan issues', + value: _failedScanCount(summary).toString(), + icon: Icons.error_outline_rounded, + ), + ], + ); + }, + ); + } + + int _failedScanCount(AnalysisSummaryUiModel summary) { + final failed = summary.scans * (100 - summary.successRatePercent) / 100; + return failed.round().clamp(0, summary.scans).toInt(); + } +} + class _ChartCard extends StatelessWidget { const _ChartCard({ - required this.title, - required this.subtitle, + required this.selectedMetric, + required this.onMetricSelected, required this.totalValue, + required this.rangeLabel, required this.chart, required this.xLabels, }); - final String title; - final String subtitle; + final AnalysisChartMetric selectedMetric; + final ValueChanged onMetricSelected; final int totalValue; + final String rangeLabel; final Widget chart; final List xLabels; @@ -150,18 +319,7 @@ class _ChartCard extends StatelessWidget { final textTheme = Theme.of(context).textTheme; return Container( - decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.06), - blurRadius: 14, - offset: const Offset(0, 6), - ), - ], - ), + decoration: _cardDecoration(context), child: Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), child: Column( @@ -175,14 +333,14 @@ class _ChartCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - title, + 'Counter trend', style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, + fontWeight: FontWeight.w800, ), ), - const SizedBox(height: 4), + const SizedBox(height: 2), Text( - subtitle, + rangeLabel, style: textTheme.bodySmall?.copyWith( color: colorScheme.onSurfaceVariant, ), @@ -190,22 +348,20 @@ class _ChartCard extends StatelessWidget { ], ), ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - totalValue.toString(), - style: textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w800, - ), - ), - const SizedBox(height: 4), - const SizedBox.shrink(), - ], + Text( + totalValue.toString(), + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w900, + ), ), ], ), const SizedBox(height: 12), + _ChartMetricSelector( + selected: selectedMetric, + onSelected: onMetricSelected, + ), + const SizedBox(height: 14), chart, if (xLabels.isNotEmpty) ...[ const SizedBox(height: 10), @@ -218,58 +374,6 @@ class _ChartCard extends StatelessWidget { } } -class _KpiGrid extends StatelessWidget { - const _KpiGrid({required this.summary}); - - final AnalysisSummaryUiModel summary; - - @override - Widget build(BuildContext context) { - return GridView.count( - crossAxisCount: 2, - crossAxisSpacing: 12, - mainAxisSpacing: 12, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - childAspectRatio: 1.5, - children: [ - _KpiCard( - title: 'Scans', - value: summary.scans.toString(), - icon: Icons.qr_code_scanner_rounded, - ), - _KpiCard( - title: 'Users', - value: summary.users.toString(), - icon: Icons.people_alt_rounded, - ), - _KpiCard( - title: 'Active Users', - value: summary.activeUsers.toString(), - icon: Icons.person_pin_circle_rounded, - ), - _KpiCard( - title: 'Points Used', - value: summary.pointsUsed.toString(), - icon: Icons.stars_rounded, - ), - _KpiCard( - title: 'Points/User', - value: summary.users == 0 - ? '0' - : (summary.pointsUsed / summary.users).toStringAsFixed(2), - icon: Icons.functions_rounded, - ), - _KpiCard( - title: 'Success Rate', - value: '${summary.successRatePercent}%', - icon: Icons.verified_rounded, - ), - ], - ); - } -} - class _ChartMetricSelector extends StatelessWidget { const _ChartMetricSelector({ required this.selected, @@ -282,6 +386,7 @@ class _ChartMetricSelector extends StatelessWidget { @override Widget build(BuildContext context) { return SegmentedButton( + showSelectedIcon: false, segments: AnalysisChartMetric.values .map( (metric) => ButtonSegment(value: metric, label: Text(metric.label)), @@ -296,6 +401,338 @@ class _ChartMetricSelector extends StatelessWidget { } } +class _OperationalNotes extends StatelessWidget { + const _OperationalNotes({required this.state}); + + final AnalysisUiState state; + + @override + Widget build(BuildContext context) { + final summary = state.summary ?? AnalysisSummaryUiModel.empty; + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + final pointsPerScan = state.pointsPerScan ?? 0; + final successRate = summary.successRatePercent; + final needsScanCheck = successRate < 98; + final hasHighRedemption = pointsPerScan >= 1.2; + + return Container( + decoration: _cardDecoration(context), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Cashier notes', + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 12), + _NoteRow( + icon: needsScanCheck + ? Icons.build_circle_rounded + : Icons.check_circle_rounded, + title: needsScanCheck ? 'Check QR retry flow' : 'QR scans stable', + body: needsScanCheck + ? 'Success is below target. Confirm lighting and member QR focus at the counter.' + : 'Scan success is within the expected cashier range.', + ), + Divider(height: 24, color: colorScheme.outlineVariant), + _NoteRow( + icon: hasHighRedemption + ? Icons.local_offer_rounded + : Icons.payments_rounded, + title: hasHighRedemption + ? 'Redemption demand is high' + : 'Redemption pace is normal', + body: hasHighRedemption + ? 'Keep reward terms visible before checkout to reduce voids.' + : 'Coin use is tracking cleanly against member scans.', + ), + ], + ), + ), + ); + } +} + +class _HeroMetric extends StatelessWidget { + const _HeroMetric({ + required this.label, + required this.value, + required this.icon, + }); + + final String label; + final String value; + final IconData icon; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Container( + constraints: const BoxConstraints(minHeight: 104), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest.withOpacity(0.42), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: colorScheme.primary, size: 22), + const SizedBox(height: 10), + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 2), + Text( + label, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} + +class _ActionMetricTile extends StatelessWidget { + const _ActionMetricTile({ + required this.width, + required this.title, + required this.value, + required this.icon, + }); + + final double width; + final String title; + final String value; + final IconData icon; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return SizedBox( + width: width, + child: Container( + constraints: const BoxConstraints(minHeight: 96), + decoration: _cardDecoration(context), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + alignment: Alignment.center, + child: Icon( + icon, + color: colorScheme.onPrimaryContainer, + size: 20, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + ), + ), + const SizedBox(height: 2), + Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _InlineStatBar extends StatelessWidget { + const _InlineStatBar({required this.items}); + + final List<_InlineStatItem> items; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return Container( + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + border: Border( + top: BorderSide(color: colorScheme.outlineVariant.withOpacity(0.7)), + ), + ), + child: Row( + children: [ + for (var i = 0; i < items.length; i++) ...[ + if (i > 0) + SizedBox( + height: 34, + child: VerticalDivider( + width: 16, + color: colorScheme.outlineVariant, + ), + ), + Expanded(child: items[i]), + ], + ], + ), + ); + } +} + +class _InlineStatItem extends StatelessWidget { + const _InlineStatItem({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w900), + ), + const SizedBox(height: 2), + Text( + label, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: textTheme.labelSmall?.copyWith( + color: colorScheme.onSurfaceVariant, + ), + ), + ], + ); + } +} + +class _StatusPill extends StatelessWidget { + const _StatusPill({required this.label, required this.icon}); + + final String label; + final IconData icon; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: colorScheme.secondaryContainer.withOpacity(0.78), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 15, color: colorScheme.onSecondaryContainer), + const SizedBox(width: 5), + Text( + label, + style: textTheme.labelMedium?.copyWith( + color: colorScheme.onSecondaryContainer, + fontWeight: FontWeight.w800, + ), + ), + ], + ), + ); + } +} + +class _NoteRow extends StatelessWidget { + const _NoteRow({required this.icon, required this.title, required this.body}); + + final IconData icon; + final String title; + final String body; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final textTheme = Theme.of(context).textTheme; + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: colorScheme.primary, size: 22), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w800, + ), + ), + const SizedBox(height: 2), + Text( + body, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + height: 1.35, + ), + ), + ], + ), + ), + ], + ); + } +} + class _XAxisLabels extends StatelessWidget { const _XAxisLabels({required this.labels}); @@ -338,215 +775,20 @@ class _XAxisLabels extends StatelessWidget { } } -class _InsightRow extends StatelessWidget { - const _InsightRow({required this.state}); - - final AnalysisUiState state; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - final averageScansPerDay = state.averageScansPerDay ?? 0; - final averagePointsPerDay = state.averagePointsPerDay ?? 0; - final pointsPerScan = state.pointsPerScan ?? 0; - final activeUserRatePercent = state.activeUserRatePercent ?? 0; - - return Wrap( - spacing: 12, - runSpacing: 12, - children: - [ - _InsightCard( - title: 'Avg scans/day', - value: averageScansPerDay.toStringAsFixed(1), - icon: Icons.stacked_line_chart_rounded, - ), - _InsightCard( - title: 'Avg points/day', - value: averagePointsPerDay.toStringAsFixed(1), - icon: Icons.trending_up_rounded, - ), - _InsightCard( - title: 'Points/scan', - value: pointsPerScan.toStringAsFixed(2), - icon: Icons.bolt_rounded, - ), - _InsightCard( - title: 'Active rate', - value: '$activeUserRatePercent%', - icon: Icons.percent_rounded, - ), - ] - .map((w) { - return SizedBox( - width: (MediaQuery.of(context).size.width - 16 * 2 - 12) / 2, - child: w, - ); - }) - .toList(growable: false), - ); - } -} - -class _InsightCard extends StatelessWidget { - const _InsightCard({ - required this.title, - required this.value, - required this.icon, - }); - - final String title; - final String value; - final IconData icon; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - - return Container( - decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.06), - blurRadius: 14, - offset: const Offset(0, 6), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), - child: Row( - children: [ - Container( - width: 38, - height: 38, - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - shape: BoxShape.circle, - ), - alignment: Alignment.center, - child: Icon( - icon, - color: colorScheme.onPrimaryContainer, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - value, - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w800, - ), - ), - const SizedBox(height: 2), - Text( - title, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), - ), - ); - } -} - -class _KpiCard extends StatelessWidget { - const _KpiCard({ - required this.title, - required this.value, - required this.icon, - }); - - final String title; - final String value; - final IconData icon; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final textTheme = Theme.of(context).textTheme; - - return Container( - decoration: BoxDecoration( - color: colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.5)), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.06), - blurRadius: 14, - offset: const Offset(0, 6), - ), - ], - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 38, - height: 38, - decoration: BoxDecoration( - color: colorScheme.primaryContainer, - shape: BoxShape.circle, - ), - alignment: Alignment.center, - child: Icon( - icon, - color: colorScheme.onPrimaryContainer, - size: 20, - ), - ), - const Spacer(), - Text( - value, - style: textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w800, - ), - ), - const SizedBox(height: 2), - Text( - title, - style: textTheme.bodySmall?.copyWith( - color: colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ); - } -} - class _LoadingBody extends StatelessWidget { const _LoadingBody(); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 40), + return const Padding( + padding: EdgeInsets.only(top: 40), child: Center( child: Column( mainAxisSize: MainAxisSize.min, - children: const [ + children: [ CircularProgressIndicator(), SizedBox(height: 12), - Text('Loading analytics...'), + Text('Loading cashier loyalty analysis...'), ], ), ), @@ -567,7 +809,7 @@ class _ErrorBody extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('Unable to load analytics'), + const Text('Unable to load cashier analysis'), const SizedBox(height: 10), TextButton(onPressed: onRetry, child: const Text('Retry')), ], @@ -576,3 +818,20 @@ class _ErrorBody extends StatelessWidget { ); } } + +BoxDecoration _cardDecoration(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return BoxDecoration( + color: colorScheme.surface, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: colorScheme.outlineVariant.withOpacity(0.55)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 12, + offset: const Offset(0, 5), + ), + ], + ); +} diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart index f19a727..4e66a4c 100644 --- a/lib/features/settings/presentation/pages/settings_page.dart +++ b/lib/features/settings/presentation/pages/settings_page.dart @@ -106,17 +106,39 @@ class _SettingsBody extends StatelessWidget { ], ), const SizedBox(height: 12), - FilledButton.icon( - onPressed: onLogout, - icon: const Icon(Icons.logout_rounded), - label: const Text('Logout'), - style: FilledButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.error, - foregroundColor: Theme.of(context).colorScheme.onError, - padding: const EdgeInsets.symmetric(vertical: 16), + _LogoutButton(onPressed: onLogout), + ], + ); + } +} + +class _LogoutButton extends StatelessWidget { + const _LogoutButton({required this.onPressed}); + + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + + return SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: onPressed, + icon: const Icon(Icons.logout_rounded), + label: const Text( + 'Logout', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + style: FilledButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), ), ), - ], + ), ); } }