stats_screen.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. import 'package:flutter/material.dart';
  2. import 'package:fl_chart/fl_chart.dart';
  3. import '../utils/colors.dart';
  4. import '../utils/score_manager.dart';
  5. class StatsScreen extends StatefulWidget {
  6. const StatsScreen({super.key});
  7. @override
  8. State<StatsScreen> createState() => _StatsScreenState();
  9. }
  10. class _StatsScreenState extends State<StatsScreen> with TickerProviderStateMixin {
  11. late TabController _tabController;
  12. bool _isLoading = true;
  13. @override
  14. void initState() {
  15. super.initState();
  16. _tabController = TabController(length: 2, vsync: this);
  17. _loadData();
  18. }
  19. @override
  20. void dispose() {
  21. _tabController.dispose();
  22. super.dispose();
  23. }
  24. Future<void> _loadData() async {
  25. // Ensure score manager is initialized
  26. await ScoreManager.initialize();
  27. setState(() {
  28. _isLoading = false;
  29. });
  30. }
  31. @override
  32. Widget build(BuildContext context) {
  33. return Scaffold(
  34. backgroundColor: ZenColors.appBackground,
  35. appBar: AppBar(
  36. backgroundColor: ZenColors.appBackground,
  37. elevation: 0,
  38. leading: IconButton(
  39. onPressed: () => Navigator.of(context).pop(true),
  40. icon: const Icon(
  41. Icons.arrow_back,
  42. color: ZenColors.primaryText,
  43. ),
  44. ),
  45. title: const Text(
  46. 'Statistics',
  47. style: TextStyle(
  48. color: ZenColors.primaryText,
  49. fontSize: 24,
  50. fontWeight: FontWeight.bold,
  51. ),
  52. ),
  53. centerTitle: true,
  54. bottom: TabBar(
  55. controller: _tabController,
  56. labelColor: ZenColors.buttonBackground,
  57. unselectedLabelColor: ZenColors.secondaryText,
  58. indicatorColor: ZenColors.buttonBackground,
  59. tabs: const [
  60. Tab(text: 'Overview'),
  61. Tab(text: 'Charts'),
  62. ],
  63. ),
  64. ),
  65. body: _isLoading
  66. ? const Center(
  67. child: CircularProgressIndicator(
  68. valueColor: AlwaysStoppedAnimation<Color>(ZenColors.buttonBackground),
  69. ),
  70. )
  71. : TabBarView(
  72. controller: _tabController,
  73. children: [
  74. _buildOverviewTab(),
  75. _buildChartsTab(),
  76. ],
  77. ),
  78. );
  79. }
  80. Widget _buildOverviewTab() {
  81. return SingleChildScrollView(
  82. padding: const EdgeInsets.all(20),
  83. child: Column(
  84. crossAxisAlignment: CrossAxisAlignment.start,
  85. children: [
  86. // Quick Stats Cards
  87. _buildQuickStatsGrid(),
  88. const SizedBox(height: 30),
  89. // Achievements Section
  90. _buildAchievementsSection(),
  91. const SizedBox(height: 30),
  92. // Recent Activity
  93. _buildRecentActivitySection(),
  94. ],
  95. ),
  96. );
  97. }
  98. Widget _buildQuickStatsGrid() {
  99. return Column(
  100. crossAxisAlignment: CrossAxisAlignment.start,
  101. children: [
  102. const Text(
  103. 'Your Relaxation Journey',
  104. style: TextStyle(
  105. color: ZenColors.primaryText,
  106. fontSize: 22,
  107. fontWeight: FontWeight.bold,
  108. ),
  109. ),
  110. const SizedBox(height: 15),
  111. GridView.count(
  112. crossAxisCount: 2,
  113. shrinkWrap: true,
  114. physics: const NeverScrollableScrollPhysics(),
  115. childAspectRatio: 1.5,
  116. crossAxisSpacing: 15,
  117. mainAxisSpacing: 15,
  118. children: [
  119. _buildStatCard(
  120. 'Today\'s Points',
  121. ScoreManager.todayScore.toString(),
  122. Icons.today,
  123. ZenColors.buttonBackground,
  124. ),
  125. _buildStatCard(
  126. 'Total Points',
  127. ScoreManager.totalScore.toString(),
  128. Icons.stars,
  129. ZenColors.red,
  130. ),
  131. _buildStatCard(
  132. 'Bubbles Popped',
  133. ScoreManager.totalBubblesPopped.toString(),
  134. Icons.bubble_chart,
  135. ZenColors.navyBlue,
  136. ),
  137. _buildStatCard(
  138. 'Daily Average',
  139. ScoreManager.averageDailyScore.toStringAsFixed(0),
  140. Icons.trending_up,
  141. Colors.green,
  142. ),
  143. _buildStatCard(
  144. 'Current Streak',
  145. '${ScoreManager.currentStreak} days',
  146. Icons.local_fire_department,
  147. Colors.orange,
  148. ),
  149. _buildStatCard(
  150. 'Best Day',
  151. ScoreManager.bestDayScore.toString(),
  152. Icons.military_tech,
  153. Colors.purple,
  154. ),
  155. ],
  156. ),
  157. ],
  158. );
  159. }
  160. Widget _buildStatCard(String title, String value, IconData icon, Color color) {
  161. return Container(
  162. padding: const EdgeInsets.all(16),
  163. decoration: BoxDecoration(
  164. color: ZenColors.uiElements.withValues(alpha: 0.3),
  165. borderRadius: BorderRadius.circular(15),
  166. border: Border.all(
  167. color: color.withValues(alpha: 0.3),
  168. width: 1,
  169. ),
  170. ),
  171. child: Column(
  172. mainAxisAlignment: MainAxisAlignment.center,
  173. children: [
  174. Icon(
  175. icon,
  176. color: color,
  177. size: 28,
  178. ),
  179. const SizedBox(height: 8),
  180. Text(
  181. value,
  182. style: const TextStyle(
  183. color: ZenColors.primaryText,
  184. fontSize: 20,
  185. fontWeight: FontWeight.bold,
  186. ),
  187. ),
  188. const SizedBox(height: 4),
  189. Text(
  190. title,
  191. style: TextStyle(
  192. color: ZenColors.secondaryText,
  193. fontSize: 12,
  194. ),
  195. textAlign: TextAlign.center,
  196. ),
  197. ],
  198. ),
  199. );
  200. }
  201. Widget _buildAchievementsSection() {
  202. final achievements = _getAchievements();
  203. return Column(
  204. crossAxisAlignment: CrossAxisAlignment.start,
  205. children: [
  206. const Text(
  207. 'Achievements',
  208. style: TextStyle(
  209. color: ZenColors.primaryText,
  210. fontSize: 22,
  211. fontWeight: FontWeight.bold,
  212. ),
  213. ),
  214. const SizedBox(height: 15),
  215. Container(
  216. height: 120,
  217. child: ListView.builder(
  218. scrollDirection: Axis.horizontal,
  219. itemCount: achievements.length,
  220. itemBuilder: (context, index) {
  221. final achievement = achievements[index];
  222. return Container(
  223. width: 100,
  224. margin: const EdgeInsets.only(right: 15),
  225. child: _buildAchievementCard(
  226. achievement['title']!,
  227. achievement['icon'] as IconData,
  228. achievement['unlocked'] as bool,
  229. ),
  230. );
  231. },
  232. ),
  233. ),
  234. ],
  235. );
  236. }
  237. Widget _buildAchievementCard(String title, IconData icon, bool unlocked) {
  238. return Container(
  239. padding: const EdgeInsets.all(12),
  240. decoration: BoxDecoration(
  241. color: unlocked
  242. ? ZenColors.buttonBackground.withValues(alpha: 0.2)
  243. : ZenColors.uiElements.withValues(alpha: 0.1),
  244. borderRadius: BorderRadius.circular(12),
  245. border: Border.all(
  246. color: unlocked
  247. ? ZenColors.buttonBackground.withValues(alpha: 0.5)
  248. : ZenColors.uiElements.withValues(alpha: 0.3),
  249. width: 1,
  250. ),
  251. ),
  252. child: Column(
  253. mainAxisAlignment: MainAxisAlignment.center,
  254. children: [
  255. Icon(
  256. icon,
  257. color: unlocked ? ZenColors.buttonBackground : ZenColors.mutedText,
  258. size: 32,
  259. ),
  260. const SizedBox(height: 8),
  261. Text(
  262. title,
  263. style: TextStyle(
  264. color: unlocked ? ZenColors.primaryText : ZenColors.mutedText,
  265. fontSize: 11,
  266. fontWeight: unlocked ? FontWeight.w600 : FontWeight.normal,
  267. ),
  268. textAlign: TextAlign.center,
  269. maxLines: 2,
  270. overflow: TextOverflow.ellipsis,
  271. ),
  272. ],
  273. ),
  274. );
  275. }
  276. List<Map<String, dynamic>> _getAchievements() {
  277. final totalScore = ScoreManager.totalScore;
  278. final streak = ScoreManager.currentStreak;
  279. final bubbles = ScoreManager.totalBubblesPopped;
  280. return [
  281. {
  282. 'title': 'First Steps',
  283. 'icon': Icons.baby_changing_station,
  284. 'unlocked': totalScore >= 10,
  285. },
  286. {
  287. 'title': 'Zen Apprentice',
  288. 'icon': Icons.self_improvement,
  289. 'unlocked': totalScore >= 100,
  290. },
  291. {
  292. 'title': 'Bubble Master',
  293. 'icon': Icons.bubble_chart,
  294. 'unlocked': bubbles >= 100,
  295. },
  296. {
  297. 'title': 'Consistent',
  298. 'icon': Icons.calendar_today,
  299. 'unlocked': streak >= 3,
  300. },
  301. {
  302. 'title': 'Dedicated',
  303. 'icon': Icons.local_fire_department,
  304. 'unlocked': streak >= 7,
  305. },
  306. {
  307. 'title': 'Zen Master',
  308. 'icon': Icons.psychology,
  309. 'unlocked': totalScore >= 1000,
  310. },
  311. ];
  312. }
  313. Widget _buildRecentActivitySection() {
  314. final lastDays = ScoreManager.getLastDaysScores(7);
  315. return Column(
  316. crossAxisAlignment: CrossAxisAlignment.start,
  317. children: [
  318. const Text(
  319. 'Last 7 Days',
  320. style: TextStyle(
  321. color: ZenColors.primaryText,
  322. fontSize: 22,
  323. fontWeight: FontWeight.bold,
  324. ),
  325. ),
  326. const SizedBox(height: 15),
  327. Container(
  328. padding: const EdgeInsets.all(16),
  329. decoration: BoxDecoration(
  330. color: ZenColors.uiElements.withValues(alpha: 0.2),
  331. borderRadius: BorderRadius.circular(15),
  332. ),
  333. child: Column(
  334. children: lastDays.map((entry) {
  335. final date = DateTime.parse(entry.key);
  336. final dayName = _getDayName(date.weekday);
  337. final isToday = _isToday(date);
  338. return Padding(
  339. padding: const EdgeInsets.symmetric(vertical: 4),
  340. child: Row(
  341. children: [
  342. SizedBox(
  343. width: 60,
  344. child: Text(
  345. dayName,
  346. style: TextStyle(
  347. color: isToday ? ZenColors.buttonBackground : ZenColors.secondaryText,
  348. fontSize: 14,
  349. fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
  350. ),
  351. ),
  352. ),
  353. Expanded(
  354. child: Container(
  355. height: 8,
  356. margin: const EdgeInsets.symmetric(horizontal: 10),
  357. child: LinearProgressIndicator(
  358. value: entry.value / (ScoreManager.bestDayScore.clamp(1, double.infinity)),
  359. backgroundColor: ZenColors.uiElements.withValues(alpha: 0.3),
  360. valueColor: AlwaysStoppedAnimation<Color>(
  361. isToday ? ZenColors.buttonBackground : ZenColors.uiElements,
  362. ),
  363. ),
  364. ),
  365. ),
  366. SizedBox(
  367. width: 50,
  368. child: Text(
  369. entry.value.toString(),
  370. style: TextStyle(
  371. color: isToday ? ZenColors.buttonBackground : ZenColors.primaryText,
  372. fontSize: 14,
  373. fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
  374. ),
  375. textAlign: TextAlign.right,
  376. ),
  377. ),
  378. ],
  379. ),
  380. );
  381. }).toList(),
  382. ),
  383. ),
  384. ],
  385. );
  386. }
  387. Widget _buildChartsTab() {
  388. return SingleChildScrollView(
  389. padding: const EdgeInsets.all(20),
  390. child: Column(
  391. crossAxisAlignment: CrossAxisAlignment.start,
  392. children: [
  393. _buildDailyChart(),
  394. const SizedBox(height: 30),
  395. _buildWeeklyChart(),
  396. ],
  397. ),
  398. );
  399. }
  400. Widget _buildDailyChart() {
  401. final lastDays = ScoreManager.getLastDaysScores(14);
  402. final maxScore = lastDays.fold(0, (max, entry) => entry.value > max ? entry.value : max).clamp(1, double.infinity);
  403. return Column(
  404. crossAxisAlignment: CrossAxisAlignment.start,
  405. children: [
  406. const Text(
  407. 'Daily Progress (Last 14 Days)',
  408. style: TextStyle(
  409. color: ZenColors.primaryText,
  410. fontSize: 22,
  411. fontWeight: FontWeight.bold,
  412. ),
  413. ),
  414. const SizedBox(height: 15),
  415. Container(
  416. height: 300,
  417. padding: const EdgeInsets.all(16),
  418. decoration: BoxDecoration(
  419. color: ZenColors.uiElements.withValues(alpha: 0.2),
  420. borderRadius: BorderRadius.circular(15),
  421. ),
  422. child: LineChart(
  423. LineChartData(
  424. backgroundColor: Colors.transparent,
  425. gridData: FlGridData(
  426. show: true,
  427. drawVerticalLine: false,
  428. getDrawingHorizontalLine: (value) {
  429. return FlLine(
  430. color: ZenColors.uiElements.withValues(alpha: 0.3),
  431. strokeWidth: 1,
  432. );
  433. },
  434. ),
  435. titlesData: FlTitlesData(
  436. bottomTitles: AxisTitles(
  437. sideTitles: SideTitles(
  438. showTitles: true,
  439. getTitlesWidget: (value, meta) {
  440. if (value.toInt() >= 0 && value.toInt() < lastDays.length) {
  441. final date = DateTime.parse(lastDays[value.toInt()].key);
  442. return Text(
  443. '${date.day}',
  444. style: const TextStyle(
  445. color: ZenColors.secondaryText,
  446. fontSize: 12,
  447. ),
  448. );
  449. }
  450. return const Text('');
  451. },
  452. ),
  453. ),
  454. leftTitles: AxisTitles(
  455. sideTitles: SideTitles(
  456. showTitles: true,
  457. reservedSize: 40,
  458. getTitlesWidget: (value, meta) {
  459. return Text(
  460. value.toInt().toString(),
  461. style: const TextStyle(
  462. color: ZenColors.secondaryText,
  463. fontSize: 12,
  464. ),
  465. );
  466. },
  467. ),
  468. ),
  469. topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  470. rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  471. ),
  472. borderData: FlBorderData(show: false),
  473. minX: 0,
  474. maxX: (lastDays.length - 1).toDouble(),
  475. minY: 0,
  476. maxY: maxScore.toDouble(),
  477. lineBarsData: [
  478. LineChartBarData(
  479. spots: lastDays.asMap().entries.map((entry) {
  480. return FlSpot(entry.key.toDouble(), entry.value.value.toDouble());
  481. }).toList(),
  482. isCurved: true,
  483. gradient: LinearGradient(
  484. colors: [
  485. ZenColors.buttonBackground.withValues(alpha: 0.8),
  486. ZenColors.buttonBackground,
  487. ],
  488. ),
  489. barWidth: 3,
  490. dotData: FlDotData(
  491. show: true,
  492. getDotPainter: (spot, percent, barData, index) {
  493. return FlDotCirclePainter(
  494. radius: 4,
  495. color: ZenColors.buttonBackground,
  496. strokeWidth: 2,
  497. strokeColor: ZenColors.primaryText,
  498. );
  499. },
  500. ),
  501. belowBarData: BarAreaData(
  502. show: true,
  503. gradient: LinearGradient(
  504. begin: Alignment.topCenter,
  505. end: Alignment.bottomCenter,
  506. colors: [
  507. ZenColors.buttonBackground.withValues(alpha: 0.3),
  508. ZenColors.buttonBackground.withValues(alpha: 0.1),
  509. ],
  510. ),
  511. ),
  512. ),
  513. ],
  514. ),
  515. ),
  516. ),
  517. ],
  518. );
  519. }
  520. Widget _buildWeeklyChart() {
  521. final weeklyScores = ScoreManager.getWeeklyScores(8);
  522. final maxScore = weeklyScores.fold(0, (max, entry) => entry.value > max ? entry.value : max).clamp(1, double.infinity);
  523. return Column(
  524. crossAxisAlignment: CrossAxisAlignment.start,
  525. children: [
  526. const Text(
  527. 'Weekly Summary (Last 8 Weeks)',
  528. style: TextStyle(
  529. color: ZenColors.primaryText,
  530. fontSize: 22,
  531. fontWeight: FontWeight.bold,
  532. ),
  533. ),
  534. const SizedBox(height: 15),
  535. Container(
  536. height: 300,
  537. padding: const EdgeInsets.all(16),
  538. decoration: BoxDecoration(
  539. color: ZenColors.uiElements.withValues(alpha: 0.2),
  540. borderRadius: BorderRadius.circular(15),
  541. ),
  542. child: BarChart(
  543. BarChartData(
  544. backgroundColor: Colors.transparent,
  545. gridData: FlGridData(
  546. show: true,
  547. drawVerticalLine: false,
  548. getDrawingHorizontalLine: (value) {
  549. return FlLine(
  550. color: ZenColors.uiElements.withValues(alpha: 0.3),
  551. strokeWidth: 1,
  552. );
  553. },
  554. ),
  555. titlesData: FlTitlesData(
  556. bottomTitles: AxisTitles(
  557. sideTitles: SideTitles(
  558. showTitles: true,
  559. getTitlesWidget: (value, meta) {
  560. if (value.toInt() >= 0 && value.toInt() < weeklyScores.length) {
  561. return Text(
  562. 'W${value.toInt() + 1}',
  563. style: const TextStyle(
  564. color: ZenColors.secondaryText,
  565. fontSize: 12,
  566. ),
  567. );
  568. }
  569. return const Text('');
  570. },
  571. ),
  572. ),
  573. leftTitles: AxisTitles(
  574. sideTitles: SideTitles(
  575. showTitles: true,
  576. reservedSize: 40,
  577. getTitlesWidget: (value, meta) {
  578. return Text(
  579. value.toInt().toString(),
  580. style: const TextStyle(
  581. color: ZenColors.secondaryText,
  582. fontSize: 12,
  583. ),
  584. );
  585. },
  586. ),
  587. ),
  588. topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  589. rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
  590. ),
  591. borderData: FlBorderData(show: false),
  592. maxY: maxScore.toDouble(),
  593. barGroups: weeklyScores.asMap().entries.map((entry) {
  594. return BarChartGroupData(
  595. x: entry.key,
  596. barRods: [
  597. BarChartRodData(
  598. toY: entry.value.value.toDouble(),
  599. gradient: LinearGradient(
  600. begin: Alignment.bottomCenter,
  601. end: Alignment.topCenter,
  602. colors: [
  603. ZenColors.navyBlue.withValues(alpha: 0.8),
  604. ZenColors.navyBlue,
  605. ],
  606. ),
  607. width: 20,
  608. borderRadius: const BorderRadius.only(
  609. topLeft: Radius.circular(6),
  610. topRight: Radius.circular(6),
  611. ),
  612. ),
  613. ],
  614. );
  615. }).toList(),
  616. ),
  617. ),
  618. ),
  619. ],
  620. );
  621. }
  622. String _getDayName(int weekday) {
  623. const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
  624. return days[weekday - 1];
  625. }
  626. bool _isToday(DateTime date) {
  627. final now = DateTime.now();
  628. return date.year == now.year && date.month == now.month && date.day == now.day;
  629. }
  630. }