import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:lunchpick/core/constants/app_colors.dart'; import 'package:lunchpick/core/constants/app_typography.dart'; import 'package:lunchpick/presentation/providers/visit_provider.dart'; import 'package:lunchpick/presentation/providers/restaurant_provider.dart'; import 'package:lunchpick/presentation/widgets/native_ad_placeholder.dart'; class VisitStatistics extends ConsumerWidget { final DateTime selectedMonth; final List availableMonths; final void Function(DateTime month) onMonthChanged; final bool adsEnabled; const VisitStatistics({ super.key, required this.selectedMonth, required this.availableMonths, required this.onMonthChanged, this.adsEnabled = true, }); @override Widget build(BuildContext context, WidgetRef ref) { final isDark = Theme.of(context).brightness == Brightness.dark; // 월별 통계 final monthlyStatsAsync = ref.watch( monthlyVisitStatsProvider(( year: selectedMonth.year, month: selectedMonth.month, )), ); final monthlyCategoryStatsAsync = ref.watch( monthlyCategoryVisitStatsProvider(( year: selectedMonth.year, month: selectedMonth.month, )), ); // 자주 방문한 맛집 final frequentRestaurantsAsync = ref.watch(frequentRestaurantsProvider); // 주간 통계 final weeklyStatsAsync = ref.watch(weeklyVisitStatsProvider); return SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ // 이번 달 통계 _buildMonthlyStats( monthlyStatsAsync, monthlyCategoryStatsAsync, isDark, ), const SizedBox(height: 16), NativeAdPlaceholder(height: 360, enabled: adsEnabled), const SizedBox(height: 16), // 주간 통계 차트 _buildWeeklyChart(weeklyStatsAsync, isDark), const SizedBox(height: 16), // 자주 방문한 맛집 TOP 3 _buildFrequentRestaurants(frequentRestaurantsAsync, ref, isDark), ], ), ); } Widget _buildMonthlyStats( AsyncValue> statsAsync, AsyncValue> categoryStatsAsync, bool isDark, ) { final monthList = _normalizeMonths(); return Card( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildMonthSelector(monthList, isDark), const SizedBox(height: 16), statsAsync.when( data: (stats) { final totalVisits = stats.values.fold( 0, (sum, count) => sum + count, ); final categoryCounts = categoryStatsAsync.maybeWhen( data: (data) { final entries = data.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); return entries; }, orElse: () => >[], ); final topCategory = categoryCounts.isNotEmpty ? categoryCounts.first : null; return Column( children: [ _buildStatItem( icon: Icons.restaurant, label: '총 방문 횟수', value: '$totalVisits회', color: AppColors.lightPrimary, isDark: isDark, ), const SizedBox(height: 12), if (topCategory != null) ...[ _buildStatItem( icon: Icons.favorite, label: '가장 많이 간 카테고리', value: '${topCategory.key} (${topCategory.value}회)', color: AppColors.lightSecondary, isDark: isDark, ), ] else ...[ _buildStatItem( icon: Icons.favorite_border, label: '가장 많이 간 카테고리', value: categoryStatsAsync.isLoading ? '집계 중...' : '데이터 없음', color: AppColors.lightSecondary, isDark: isDark, ), ], ], ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Text('통계를 불러올 수 없습니다', style: AppTypography.body2(isDark)), ), ], ), ), ); } Widget _buildWeeklyChart( AsyncValue> statsAsync, bool isDark, ) { return Card( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('최근 7일 방문 현황', style: AppTypography.heading2(isDark)), const SizedBox(height: 16), statsAsync.when( data: (stats) { final maxCount = stats.values.isEmpty ? 1 : stats.values.reduce((a, b) => a > b ? a : b); return SizedBox( height: 140, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.end, children: stats.entries.map((entry) { final height = maxCount == 0 ? 0.0 : (entry.value / maxCount) * 80; return Column( mainAxisAlignment: MainAxisAlignment.end, children: [ Text( entry.value.toString(), style: AppTypography.caption(isDark), ), const SizedBox(height: 4), Container( width: 30, height: height, decoration: BoxDecoration( color: AppColors.lightPrimary, borderRadius: BorderRadius.circular(4), ), ), const SizedBox(height: 4), Text(entry.key, style: AppTypography.caption(isDark)), ], ); }).toList(), ), ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Text('차트를 불러올 수 없습니다', style: AppTypography.body2(isDark)), ), ], ), ), ); } Widget _buildFrequentRestaurants( AsyncValue> frequentAsync, WidgetRef ref, bool isDark, ) { return Card( color: isDark ? AppColors.darkSurface : AppColors.lightSurface, elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('자주 방문한 맛집 TOP 3', style: AppTypography.heading2(isDark)), const SizedBox(height: 16), frequentAsync.when( data: (frequentList) { if (frequentList.isEmpty) { return Center( child: Text( '아직 방문 기록이 없습니다', style: AppTypography.body2(isDark), ), ); } return Column( children: frequentList.take(3).map((item) { final restaurantAsync = ref.watch( restaurantProvider(item.restaurantId), ); return restaurantAsync.when( data: (restaurant) { if (restaurant == null) { return const SizedBox.shrink(); } return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: AppColors.lightPrimary .withOpacity(0.1), shape: BoxShape.circle, ), child: Center( child: Text( '${frequentList.indexOf(item) + 1}', style: AppTypography.body1(isDark) .copyWith( color: AppColors.lightPrimary, fontWeight: FontWeight.bold, ), ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( restaurant.name, style: AppTypography.body1(isDark) .copyWith( fontWeight: FontWeight.w500, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( restaurant.category, style: AppTypography.caption( isDark, ), ), ], ), ), Text( '${item.visitCount}회', style: AppTypography.body2(isDark) .copyWith( color: AppColors.lightPrimary, fontWeight: FontWeight.bold, ), ), ], ), ); }, loading: () => const SizedBox(height: 44), error: (error, stack) => const SizedBox.shrink(), ); }).toList() as List, ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Text('데이터를 불러올 수 없습니다', style: AppTypography.body2(isDark)), ), ], ), ), ); } Widget _buildStatItem({ required IconData icon, required String label, required String value, required Color color, required bool isDark, }) { return Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: color.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon(icon, color: color, size: 20), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(label, style: AppTypography.caption(isDark)), Text( value, style: AppTypography.body1( isDark, ).copyWith(fontWeight: FontWeight.bold), ), ], ), ), ], ); } Widget _buildMonthSelector(List months, bool isDark) { final currentMonth = DateTime(selectedMonth.year, selectedMonth.month, 1); final monthIndex = months.indexWhere( (month) => _isSameMonth(month, currentMonth), ); final resolvedIndex = monthIndex == -1 ? 0 : monthIndex; final hasPrevious = resolvedIndex < months.length - 1; final hasNext = resolvedIndex > 0; return Row( children: [ Expanded( child: Text( '${_formatMonth(currentMonth)} 방문 통계', style: AppTypography.heading2(isDark), overflow: TextOverflow.ellipsis, ), ), IconButton( onPressed: hasPrevious ? () => onMonthChanged(months[resolvedIndex + 1]) : null, icon: const Icon(Icons.chevron_left), ), DropdownButtonHideUnderline( child: DropdownButton( value: months[resolvedIndex], onChanged: (month) { if (month != null) { onMonthChanged(month); } }, items: months .map( (month) => DropdownMenuItem( value: month, child: Text(_formatMonth(month)), ), ) .toList(), ), ), IconButton( onPressed: hasNext ? () => onMonthChanged(months[resolvedIndex - 1]) : null, icon: const Icon(Icons.chevron_right), ), ], ); } List _normalizeMonths() { final normalized = availableMonths .map((month) => DateTime(month.year, month.month, 1)) .toSet() .toList() ..sort((a, b) => b.compareTo(a)); final currentMonth = DateTime(selectedMonth.year, selectedMonth.month, 1); final exists = normalized.any((month) => _isSameMonth(month, currentMonth)); if (!exists) { normalized.insert(0, currentMonth); } return normalized; } bool _isSameMonth(DateTime a, DateTime b) => a.year == b.year && a.month == b.month; String _formatMonth(DateTime month) => '${month.year}.${month.month.toString().padLeft(2, '0')}'; }