import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../core/constants.dart'; import 'provider.dart'; final _currencyFmt = NumberFormat.currency(symbol: '\$', decimalDigits: 2); enum ChartType { pie, bar } class CategoriesScreen extends ConsumerStatefulWidget { const CategoriesScreen({super.key}); @override ConsumerState createState() => _CategoriesScreenState(); } class _CategoriesScreenState extends ConsumerState { int _touchedIndex = -1; ChartType _chartType = ChartType.pie; @override Widget build(BuildContext context) { final data = ref.watch(categoryExpenseProvider); final monthlyData = ref.watch(monthlyBreakdownProvider); final total = data.values.fold(0.0, (a, b) => a + b); // Sort categories by amount descending final sortedEntries = data.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); return Scaffold( backgroundColor: AppColors.background, body: SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Categories', style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), Text( 'Expense breakdown', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppColors.textSecondary, ), ), ], ), ), _ChartToggle( selected: _chartType, onChanged: (t) => setState(() => _chartType = t), ), ], ), const SizedBox(height: 24), if (data.isEmpty) const Expanded(child: _EmptyState()) else Expanded( child: ListView( children: [ if (_chartType == ChartType.pie) _PieChartCard( data: data, total: total, touchedIndex: _touchedIndex, onTouch: (i) => setState(() => _touchedIndex = i), ) else _BarChartCard(monthlyData: monthlyData), const SizedBox(height: 20), Text( 'Ranked by Amount', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 12), ...sortedEntries.asMap().entries.map((entry) { final rank = entry.key + 1; final cat = entry.value.key; final amount = entry.value.value; return Padding( padding: const EdgeInsets.only(bottom: 10), child: _CategoryRow( rank: rank, category: cat, amount: amount, total: total, ), ); }), const SizedBox(height: 80), ], ), ), ], ), ), ), ); } } class _ChartToggle extends StatelessWidget { final ChartType selected; final ValueChanged onChanged; const _ChartToggle({required this.selected, required this.onChanged}); @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColors.divider), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ _ToggleButton( icon: Icons.pie_chart_rounded, isSelected: selected == ChartType.pie, onTap: () => onChanged(ChartType.pie), ), _ToggleButton( icon: Icons.bar_chart_rounded, isSelected: selected == ChartType.bar, onTap: () => onChanged(ChartType.bar), ), ], ), ); } } class _ToggleButton extends StatelessWidget { final IconData icon; final bool isSelected; final VoidCallback onTap; const _ToggleButton({ required this.icon, required this.isSelected, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: isSelected ? AppColors.accent.withOpacity(0.15) : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Icon( icon, color: isSelected ? AppColors.accent : AppColors.textSecondary, size: 20, ), ), ); } } class _PieChartCard extends StatelessWidget { final Map data; final double total; final int touchedIndex; final ValueChanged onTouch; const _PieChartCard({ required this.data, required this.total, required this.touchedIndex, required this.onTouch, }); @override Widget build(BuildContext context) { final entries = data.entries.toList(); return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.divider), ), child: Column( children: [ SizedBox( height: 220, child: Stack( alignment: Alignment.center, children: [ PieChart( PieChartData( pieTouchData: PieTouchData( touchCallback: (event, response) { if (!event.isInterestedForInteractions || response == null || response.touchedSection == null) { onTouch(-1); return; } onTouch(response.touchedSection!.touchedSectionIndex); }, ), sectionsSpace: 3, centerSpaceRadius: 60, sections: List.generate(entries.length, (i) { final isTouched = i == touchedIndex; final cat = entries[i].key; final val = entries[i].value; final color = AppCategories.colors[cat] ?? AppColors.accent; return PieChartSectionData( color: color, value: val, title: isTouched ? '${(val / total * 100).toStringAsFixed(1)}%' : '', radius: isTouched ? 60 : 50, titleStyle: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: Colors.white, ), ); }), ), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Total', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ), ), Text( _currencyFmt.format(total), style: Theme.of(context).textTheme.titleMedium?.copyWith( color: AppColors.textPrimary, fontWeight: FontWeight.w700, ), ), ], ), ], ), ), ], ), ); } } class _BarChartCard extends StatelessWidget { final List monthlyData; const _BarChartCard({required this.monthlyData}); @override Widget build(BuildContext context) { final maxY = monthlyData.map((e) => e.amount).reduce((a, b) => a > b ? a : b); final adjustedMaxY = maxY * 1.2; return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColors.divider), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Last 6 Months', style: Theme.of(context).textTheme.titleSmall?.copyWith( color: AppColors.textSecondary, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 16), SizedBox( height: 200, child: BarChart( BarChartData( alignment: BarChartAlignment.spaceAround, maxY: adjustedMaxY > 0 ? adjustedMaxY : 100, barTouchData: BarTouchData( touchTooltipData: BarTouchTooltipData( getTooltipItem: (group, groupIndex, rod, rodIndex) { return BarTooltipItem( _currencyFmt.format(rod.toY), const TextStyle( color: Colors.white, fontWeight: FontWeight.w600, fontSize: 12, ), ); }, ), ), titlesData: FlTitlesData( show: true, bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, meta) { if (value.toInt() >= 0 && value.toInt() < monthlyData.length) { final month = monthlyData[value.toInt()].month; return Padding( padding: const EdgeInsets.only(top: 8), child: Text( DateFormat('MMM').format(month), style: const TextStyle( color: AppColors.textSecondary, fontSize: 11, ), ), ); } return const Text(''); }, ), ), leftTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), topTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), rightTitles: const AxisTitles( sideTitles: SideTitles(showTitles: false), ), ), gridData: FlGridData( show: true, drawVerticalLine: false, horizontalInterval: adjustedMaxY > 0 ? adjustedMaxY / 4 : 25, getDrawingHorizontalLine: (value) => FlLine( color: AppColors.divider, strokeWidth: 1, ), ), borderData: FlBorderData(show: false), barGroups: List.generate( monthlyData.length, (i) => BarChartGroupData( x: i, barRods: [ BarChartRodData( toY: monthlyData[i].amount, color: AppColors.accent, width: 24, borderRadius: const BorderRadius.vertical( top: Radius.circular(6), ), ), ], ), ), ), ), ), ], ), ); } } class _CategoryRow extends StatelessWidget { final int rank; final String category; final double amount; final double total; const _CategoryRow({ required this.rank, required this.category, required this.amount, required this.total, }); @override Widget build(BuildContext context) { final color = AppCategories.colors[category] ?? AppColors.accent; final icon = AppCategories.icons[category] ?? Icons.category_rounded; final pct = total > 0 ? amount / total : 0.0; return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.divider), ), child: Column( children: [ Row( children: [ Container( width: 28, height: 28, alignment: Alignment.center, decoration: BoxDecoration( color: rank <= 3 ? color.withOpacity(0.2) : AppColors.divider, shape: BoxShape.circle, ), child: Text( '$rank', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: rank <= 3 ? color : AppColors.textSecondary, fontWeight: FontWeight.w700, ), ), ), const SizedBox(width: 10), Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: color.withOpacity(0.15), borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: color, size: 18), ), const SizedBox(width: 12), Expanded( child: Text( category, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( _currencyFmt.format(amount), style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppColors.expense, fontWeight: FontWeight.w700, ), ), Text( '${(pct * 100).toStringAsFixed(1)}%', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.textSecondary, ), ), ], ), ], ), const SizedBox(height: 10), ClipRRect( borderRadius: BorderRadius.circular(4), child: LinearProgressIndicator( value: pct, backgroundColor: AppColors.divider, valueColor: AlwaysStoppedAnimation(color), minHeight: 6, ), ), ], ), ); } } class _EmptyState extends StatelessWidget { const _EmptyState(); @override Widget build(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: const EdgeInsets.all(24), decoration: const BoxDecoration( color: AppColors.surface, shape: BoxShape.circle, ), child: const Icon( Icons.pie_chart_outline_rounded, size: 48, color: AppColors.textSecondary, ), ), const SizedBox(height: 16), Text( 'No expense data', style: Theme.of(context).textTheme.titleMedium?.copyWith( color: AppColors.textPrimary, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 6), Text( 'Add some expenses to see the breakdown', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppColors.textSecondary, ), ), ], ), ); } }