Files
lunchpick/lib/presentation/pages/calendar/widgets/visit_statistics.dart

333 lines
13 KiB
Dart

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;
const VisitStatistics({super.key, required this.selectedMonth});
@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 frequentRestaurantsAsync = ref.watch(frequentRestaurantsProvider);
// 주간 통계
final weeklyStatsAsync = ref.watch(weeklyVisitStatsProvider);
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 이번 달 통계
_buildMonthlyStats(monthlyStatsAsync, isDark),
const SizedBox(height: 16),
const NativeAdPlaceholder(height: 360),
const SizedBox(height: 16),
// 주간 통계 차트
_buildWeeklyChart(weeklyStatsAsync, isDark),
const SizedBox(height: 16),
// 자주 방문한 맛집 TOP 3
_buildFrequentRestaurants(frequentRestaurantsAsync, ref, isDark),
],
),
);
}
Widget _buildMonthlyStats(
AsyncValue<Map<String, int>> 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(
'${selectedMonth.month}월 방문 통계',
style: AppTypography.heading2(isDark),
),
const SizedBox(height: 16),
statsAsync.when(
data: (stats) {
final totalVisits = stats.values.fold(
0,
(sum, count) => sum + count,
);
final categoryCounts =
stats.entries.where((e) => !e.key.contains('/')).toList()
..sort((a, b) => b.value.compareTo(a.value));
return Column(
children: [
_buildStatItem(
icon: Icons.restaurant,
label: '총 방문 횟수',
value: '$totalVisits회',
color: AppColors.lightPrimary,
isDark: isDark,
),
const SizedBox(height: 12),
if (categoryCounts.isNotEmpty) ...[
_buildStatItem(
icon: Icons.favorite,
label: '가장 많이 간 카테고리',
value:
'${categoryCounts.first.key} (${categoryCounts.first.value}회)',
color: AppColors.lightSecondary,
isDark: isDark,
),
],
],
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) =>
Text('통계를 불러올 수 없습니다', style: AppTypography.body2(isDark)),
),
],
),
),
);
}
Widget _buildWeeklyChart(
AsyncValue<Map<String, int>> 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<List<({String restaurantId, int visitCount})>> 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<Widget>,
);
},
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),
),
],
),
),
],
);
}
}