cb_prestige_qr/lib/features/analysis/presentation/pages/analysis_page.dart

838 lines
25 KiB
Dart
Raw Normal View History

2026-04-09 06:47:03 +00:00
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
2026-04-23 04:29:34 +00:00
2026-04-09 06:47:03 +00:00
import '../manager/analysis_chart_metric.dart';
2026-04-23 04:29:34 +00:00
import '../manager/analysis_range.dart';
2026-04-09 06:47:03 +00:00
import '../manager/analysis_ui_state.dart';
import '../manager/analysis_view_model.dart';
import '../widgets/analysis_line_chart.dart';
class AnalysisPage extends ConsumerWidget {
const AnalysisPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final rangePreset = ref.watch(analysisRangeNotifierProvider);
final chartMetric = ref.watch(analysisChartMetricNotifierProvider);
final stateAsync = ref.watch(analysisViewModelProvider);
return Scaffold(
backgroundColor: theme.scaffoldBackgroundColor,
appBar: AppBar(
2026-04-23 04:29:34 +00:00
title: const Text('Cashier Analysis'),
2026-04-09 06:47:03 +00:00
backgroundColor: theme.scaffoldBackgroundColor,
surfaceTintColor: theme.scaffoldBackgroundColor,
elevation: 0,
scrolledUnderElevation: 0,
),
body: RefreshIndicator(
onRefresh: () async => ref.invalidate(analysisViewModelProvider),
child: ListView(
physics: const AlwaysScrollableScrollPhysics(),
2026-04-23 04:29:34 +00:00
padding: const EdgeInsets.fromLTRB(16, 8, 16, 96),
2026-04-09 06:47:03 +00:00
children: [
_RangeSelector(
selected: rangePreset,
onSelected: (preset) => ref
.read(analysisRangeNotifierProvider.notifier)
.setRange(preset),
),
const SizedBox(height: 12),
stateAsync.when(
2026-04-23 04:29:34 +00:00
data: (state) => _AnalysisBody(
state: state,
chartMetric: chartMetric,
onMetricSelected: (metric) => ref
.read(analysisChartMetricNotifierProvider.notifier)
.setMetric(metric),
),
2026-04-09 06:47:03 +00:00
loading: () => const _LoadingBody(),
error: (error, stackTrace) => _ErrorBody(
onRetry: () => ref.invalidate(analysisViewModelProvider),
),
),
],
),
),
);
}
}
class _RangeSelector extends StatelessWidget {
const _RangeSelector({required this.selected, required this.onSelected});
final AnalysisRangePreset selected;
final ValueChanged<AnalysisRangePreset> onSelected;
@override
Widget build(BuildContext context) {
return SegmentedButton<AnalysisRangePreset>(
2026-04-23 04:29:34 +00:00
showSelectedIcon: false,
2026-04-09 06:47:03 +00:00
segments: AnalysisRangePreset.values
.map(
(preset) => ButtonSegment(value: preset, label: Text(preset.label)),
)
.toList(growable: false),
selected: <AnalysisRangePreset>{selected},
onSelectionChanged: (selection) {
if (selection.isEmpty) return;
onSelected(selection.first);
},
);
}
}
class _AnalysisBody extends StatelessWidget {
2026-04-23 04:29:34 +00:00
const _AnalysisBody({
required this.state,
required this.chartMetric,
required this.onMetricSelected,
});
2026-04-09 06:47:03 +00:00
final AnalysisUiState state;
final AnalysisChartMetric chartMetric;
2026-04-23 04:29:34 +00:00
final ValueChanged<AnalysisChartMetric> onMetricSelected;
2026-04-09 06:47:03 +00:00
@override
Widget build(BuildContext context) {
final summary = state.summary ?? AnalysisSummaryUiModel.empty;
final series = state.series ?? const <AnalysisSeriesPointUiModel>[];
final values = series
.map(
(p) => switch (chartMetric) {
AnalysisChartMetric.scans => p.scanCount.toDouble(),
AnalysisChartMetric.pointsUsed => p.pointsUsed.toDouble(),
},
)
.toList(growable: false);
final totalValue = switch (chartMetric) {
AnalysisChartMetric.scans => summary.scans,
AnalysisChartMetric.pointsUsed => summary.pointsUsed,
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
2026-04-23 04:29:34 +00:00
_ShiftSummaryCard(state: state),
const SizedBox(height: 12),
_CashierActionGrid(summary: summary, state: state),
const SizedBox(height: 12),
2026-04-09 06:47:03 +00:00
_ChartCard(
2026-04-23 04:29:34 +00:00
selectedMetric: chartMetric,
onMetricSelected: onMetricSelected,
2026-04-09 06:47:03 +00:00
totalValue: totalValue,
2026-04-23 04:29:34 +00:00
rangeLabel: state.rangeLabel ?? '',
chart: AnalysisLineChart(values: values, height: 132),
xLabels: series.map((p) => p.label).toList(growable: false),
2026-04-09 06:47:03 +00:00
),
const SizedBox(height: 12),
2026-04-23 04:29:34 +00:00
_OperationalNotes(state: state),
2026-04-09 06:47:03 +00:00
],
);
}
}
2026-04-23 04:29:34 +00:00
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();
}
}
2026-04-09 06:47:03 +00:00
class _ChartCard extends StatelessWidget {
const _ChartCard({
2026-04-23 04:29:34 +00:00
required this.selectedMetric,
required this.onMetricSelected,
2026-04-09 06:47:03 +00:00
required this.totalValue,
2026-04-23 04:29:34 +00:00
required this.rangeLabel,
2026-04-09 06:47:03 +00:00
required this.chart,
required this.xLabels,
});
2026-04-23 04:29:34 +00:00
final AnalysisChartMetric selectedMetric;
final ValueChanged<AnalysisChartMetric> onMetricSelected;
2026-04-09 06:47:03 +00:00
final int totalValue;
2026-04-23 04:29:34 +00:00
final String rangeLabel;
2026-04-09 06:47:03 +00:00
final Widget chart;
final List<String> xLabels;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Container(
2026-04-23 04:29:34 +00:00
decoration: _cardDecoration(context),
2026-04-09 06:47:03 +00:00
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
2026-04-23 04:29:34 +00:00
'Counter trend',
2026-04-09 06:47:03 +00:00
style: textTheme.titleMedium?.copyWith(
2026-04-23 04:29:34 +00:00
fontWeight: FontWeight.w800,
2026-04-09 06:47:03 +00:00
),
),
2026-04-23 04:29:34 +00:00
const SizedBox(height: 2),
2026-04-09 06:47:03 +00:00
Text(
2026-04-23 04:29:34 +00:00
rangeLabel,
2026-04-09 06:47:03 +00:00
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
2026-04-23 04:29:34 +00:00
Text(
totalValue.toString(),
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w900,
),
2026-04-09 06:47:03 +00:00
),
],
),
const SizedBox(height: 12),
2026-04-23 04:29:34 +00:00
_ChartMetricSelector(
selected: selectedMetric,
onSelected: onMetricSelected,
),
const SizedBox(height: 14),
2026-04-09 06:47:03 +00:00
chart,
if (xLabels.isNotEmpty) ...[
const SizedBox(height: 10),
_XAxisLabels(labels: xLabels),
],
],
),
),
);
}
}
class _ChartMetricSelector extends StatelessWidget {
const _ChartMetricSelector({
required this.selected,
required this.onSelected,
});
final AnalysisChartMetric selected;
final ValueChanged<AnalysisChartMetric> onSelected;
@override
Widget build(BuildContext context) {
return SegmentedButton<AnalysisChartMetric>(
2026-04-23 04:29:34 +00:00
showSelectedIcon: false,
2026-04-09 06:47:03 +00:00
segments: AnalysisChartMetric.values
.map(
(metric) => ButtonSegment(value: metric, label: Text(metric.label)),
)
.toList(growable: false),
selected: <AnalysisChartMetric>{selected},
onSelectionChanged: (selection) {
if (selection.isEmpty) return;
onSelected(selection.first);
},
);
}
}
2026-04-23 04:29:34 +00:00
class _OperationalNotes extends StatelessWidget {
const _OperationalNotes({required this.state});
2026-04-09 06:47:03 +00:00
2026-04-23 04:29:34 +00:00
final AnalysisUiState state;
2026-04-09 06:47:03 +00:00
@override
Widget build(BuildContext context) {
2026-04-23 04:29:34 +00:00
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;
2026-04-09 06:47:03 +00:00
2026-04-23 04:29:34 +00:00
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,
),
2026-04-09 06:47:03 +00:00
),
2026-04-23 04:29:34 +00:00
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.',
),
],
),
),
2026-04-09 06:47:03 +00:00
);
}
}
2026-04-23 04:29:34 +00:00
class _HeroMetric extends StatelessWidget {
const _HeroMetric({
required this.label,
required this.value,
required this.icon,
});
2026-04-09 06:47:03 +00:00
2026-04-23 04:29:34 +00:00
final String label;
final String value;
final IconData icon;
2026-04-09 06:47:03 +00:00
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
2026-04-23 04:29:34 +00:00
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,
),
),
],
),
2026-04-09 06:47:03 +00:00
);
}
}
2026-04-23 04:29:34 +00:00
class _ActionMetricTile extends StatelessWidget {
const _ActionMetricTile({
required this.width,
2026-04-09 06:47:03 +00:00
required this.title,
required this.value,
required this.icon,
});
2026-04-23 04:29:34 +00:00
final double width;
2026-04-09 06:47:03 +00:00
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;
2026-04-23 04:29:34 +00:00
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,
),
2026-04-09 06:47:03 +00:00
),
2026-04-23 04:29:34 +00:00
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,
),
2026-04-09 06:47:03 +00:00
),
2026-04-23 04:29:34 +00:00
const SizedBox(height: 2),
Text(
title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
2026-04-09 06:47:03 +00:00
),
2026-04-23 04:29:34 +00:00
],
),
2026-04-09 06:47:03 +00:00
),
2026-04-23 04:29:34 +00:00
],
),
2026-04-09 06:47:03 +00:00
),
),
);
}
}
2026-04-23 04:29:34 +00:00
class _InlineStatBar extends StatelessWidget {
const _InlineStatBar({required this.items});
2026-04-09 06:47:03 +00:00
2026-04-23 04:29:34 +00:00
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;
2026-04-09 06:47:03 +00:00
final String value;
2026-04-23 04:29:34 +00:00
@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;
2026-04-09 06:47:03 +00:00
final IconData icon;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Container(
2026-04-23 04:29:34 +00:00
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 7),
2026-04-09 06:47:03 +00:00
decoration: BoxDecoration(
2026-04-23 04:29:34 +00:00
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,
),
2026-04-09 06:47:03 +00:00
),
],
),
2026-04-23 04:29:34 +00:00
);
}
}
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,
),
2026-04-09 06:47:03 +00:00
),
2026-04-23 04:29:34 +00:00
const SizedBox(height: 2),
Text(
body,
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
height: 1.35,
),
2026-04-09 06:47:03 +00:00
),
2026-04-23 04:29:34 +00:00
],
),
2026-04-09 06:47:03 +00:00
),
2026-04-23 04:29:34 +00:00
],
);
}
}
class _XAxisLabels extends StatelessWidget {
const _XAxisLabels({required this.labels});
final List<String> labels;
@override
Widget build(BuildContext context) {
final textStyle = Theme.of(context).textTheme.labelSmall;
final onSurfaceVariant = Theme.of(context).colorScheme.onSurfaceVariant;
final count = labels.length;
final desiredTicks = 4;
final step = count <= desiredTicks ? 1 : (count - 1) ~/ (desiredTicks - 1);
final ticks = <int>{0, count - 1};
for (var i = 0; i < count; i += step) {
ticks.add(i);
}
return Row(
children: List.generate(count, (i) {
final show = ticks.contains(i);
return Expanded(
child: Align(
alignment: i == 0
? Alignment.centerLeft
: i == count - 1
? Alignment.centerRight
: Alignment.center,
child: Text(
show ? labels[i] : '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textStyle?.copyWith(color: onSurfaceVariant),
),
),
);
}),
2026-04-09 06:47:03 +00:00
);
}
}
class _LoadingBody extends StatelessWidget {
const _LoadingBody();
@override
Widget build(BuildContext context) {
2026-04-23 04:29:34 +00:00
return const Padding(
padding: EdgeInsets.only(top: 40),
2026-04-09 06:47:03 +00:00
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
2026-04-23 04:29:34 +00:00
children: [
2026-04-09 06:47:03 +00:00
CircularProgressIndicator(),
SizedBox(height: 12),
2026-04-23 04:29:34 +00:00
Text('Loading cashier loyalty analysis...'),
2026-04-09 06:47:03 +00:00
],
),
),
);
}
}
class _ErrorBody extends StatelessWidget {
const _ErrorBody({required this.onRetry});
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 40),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
2026-04-23 04:29:34 +00:00
const Text('Unable to load cashier analysis'),
2026-04-09 06:47:03 +00:00
const SizedBox(height: 10),
TextButton(onPressed: onRetry, child: const Text('Retry')),
],
),
),
);
}
}
2026-04-23 04:29:34 +00:00
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),
),
],
);
}