From 9fc61c228054b9a37a45d7fe7bf6c518a98a20c6 Mon Sep 17 00:00:00 2001 From: SNFASA Date: Tue, 24 Feb 2026 23:22:56 +0800 Subject: [PATCH 1/6] update --- lib/features/home/models/user_model.dart | 4 + .../home/repositories/scan_repository.dart | 62 ++++++++++--- .../repositories/scoreboard_repository.dart | 10 +- lib/features/home/ui/scoreboard_screen.dart | 2 +- lib/features/home/ui/{text.dart => text.txt} | 91 ++++++++++++++++++- 5 files changed, 150 insertions(+), 19 deletions(-) rename lib/features/home/ui/{text.dart => text.txt} (93%) diff --git a/lib/features/home/models/user_model.dart b/lib/features/home/models/user_model.dart index e6712c6..9a10c66 100644 --- a/lib/features/home/models/user_model.dart +++ b/lib/features/home/models/user_model.dart @@ -11,6 +11,7 @@ class UserModel { final int streak; final Map categoryCounts; final double nextMilestoneCo2; + final String lastScanWeekId; UserModel({ required this.id, @@ -25,6 +26,7 @@ class UserModel { required this.streak, required this.categoryCounts, required this.nextMilestoneCo2, + required this.lastScanWeekId, }); factory UserModel.fromMap(String id, Map json) { @@ -41,6 +43,7 @@ class UserModel { streak: json['streak'] ?? 0, categoryCounts: Map.from(json['categoryCounts'] ?? {}), nextMilestoneCo2: (json['nextMilestoneCo2'] ?? 20.0).toDouble(), + lastScanWeekId: json['lastScanWeekId'] ?? '', ); } @@ -57,6 +60,7 @@ class UserModel { 'streak': streak, 'categoryCounts': categoryCounts, 'nextMilestoneCo2': nextMilestoneCo2, + 'lastScanWeekId': lastScanWeekId, }; } } \ No newline at end of file diff --git a/lib/features/home/repositories/scan_repository.dart b/lib/features/home/repositories/scan_repository.dart index 48ac1a9..ec3481e 100644 --- a/lib/features/home/repositories/scan_repository.dart +++ b/lib/features/home/repositories/scan_repository.dart @@ -16,20 +16,58 @@ class ScanRepository { final userRef = _firestore.collection('users').doc(uid); final scanRef = userRef.collection('scans').doc(); - WriteBatch batch = _firestore.batch(); + try { + // Fetch the current user data to check the last recorded week + final userDoc = await userRef.get(); + + WriteBatch batch = _firestore.batch(); - // 1. Save scan details - batch.set(scanRef, scan.toMap()); + // 1. Save individual scan details + batch.set(scanRef, scan.toMap()); - // 2. Update user's aggregate stats and check week reset logic - // Inside ScanRepository.saveScan() - batch.update(userRef, { - 'categoryCounts.${scan.category}': FieldValue.increment(1), // Updates the specific category - 'totalScans': FieldValue.increment(1), - 'ecoPoints': FieldValue.increment(scan.pointsEarned), - 'co2Offset': FieldValue.increment(scan.co2Saved), - }); + // 2. Weekly Reset Logic + // We check if the 'lastScanWeekId' in Firestore matches the scan's current 'weekId' + bool isNewWeek = true; + if (userDoc.exists) { + final data = userDoc.data() as Map; + final String lastWeekId = data['lastScanWeekId'] ?? ""; + + // If the stored week ID matches the current scan's week ID, it's NOT a new week + if (lastWeekId == scan.weekId) { + isNewWeek = false; + } + } - await batch.commit(); + // 3. Update user's aggregate stats + if (isNewWeek) { + // RESET: Start weekly points fresh for the new week + batch.update(userRef, { + 'categoryCounts.${scan.category}': FieldValue.increment(1), + 'totalScans': FieldValue.increment(1), + 'ecoPoints': FieldValue.increment(scan.pointsEarned), + 'weeklyPoints': scan.pointsEarned, // Set to current scan points (Overwrite old week) + 'co2Offset': FieldValue.increment(scan.co2Saved), + 'lastScanWeekId': scan.weekId, // Update the tracking week ID + }); + } else { + // INCREMENT: Add to existing weekly points + batch.update(userRef, { + 'categoryCounts.${scan.category}': FieldValue.increment(1), + 'totalScans': FieldValue.increment(1), + 'ecoPoints': FieldValue.increment(scan.pointsEarned), + 'weeklyPoints': FieldValue.increment(scan.pointsEarned), // Correctly updating the scoreboard + 'co2Offset': FieldValue.increment(scan.co2Saved), + 'lastScanWeekId': scan.weekId, + }); + } + + // Commit all changes at once + await batch.commit(); + + } catch (e) { + // Log error for debugging + print("Error saving scan and updating scoreboard: $e"); + rethrow; + } } } \ No newline at end of file diff --git a/lib/features/home/repositories/scoreboard_repository.dart b/lib/features/home/repositories/scoreboard_repository.dart index 6a200ce..3cd1d56 100644 --- a/lib/features/home/repositories/scoreboard_repository.dart +++ b/lib/features/home/repositories/scoreboard_repository.dart @@ -10,11 +10,11 @@ class ScoreboardRepository { .collection('users') .orderBy('weeklyPoints', descending: true) .limit(limit) - .snapshots() // <-- This is the magic word for real-time updates! + .snapshots() .map((querySnapshot) { - return querySnapshot.docs.map((doc) { - return UserModel.fromMap(doc.id, doc.data()); - }).toList(); - }); + return querySnapshot.docs.map((doc) { + return UserModel.fromMap(doc.id, doc.data()); + }).toList(); + }); } } \ No newline at end of file diff --git a/lib/features/home/ui/scoreboard_screen.dart b/lib/features/home/ui/scoreboard_screen.dart index ad5f504..7edbfa0 100644 --- a/lib/features/home/ui/scoreboard_screen.dart +++ b/lib/features/home/ui/scoreboard_screen.dart @@ -38,7 +38,7 @@ class ScoreboardScreen extends ConsumerWidget { return Center( child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 800), + constraints: const BoxConstraints(maxWidth: 1000), child: SingleChildScrollView( child: Column( children: [ diff --git a/lib/features/home/ui/text.dart b/lib/features/home/ui/text.txt similarity index 93% rename from lib/features/home/ui/text.dart rename to lib/features/home/ui/text.txt index 0d1a9eb..931ab15 100644 --- a/lib/features/home/ui/text.dart +++ b/lib/features/home/ui/text.txt @@ -197,4 +197,93 @@ class ScoreboardScreen extends StatelessWidget { decoration: BoxDecoration(shape: BoxShape.circle, color: color), ); } -} \ No newline at end of file +} + + +createdAt +February 19, 2026 at 1:00:58 AM UTC+8 +(timestamp) + + +ecoPoints +5420 +(number) + + +email +"nabil.0413@gmail.com" +(string) + + +lastActive +February 19, 2026 at 1:01:19 AM UTC+8 +(timestamp) + + +rankTier +"Gold" +(string) + + +streak +5 +(number) + + +totalScans +85 +(number) + + +username +"Syed Nabil" +(string) + + +weekId +"2026-W07" +(string) + + +weeklyPoints +320 + + + +category +"plastic" +(string) + + +co2Saved +0.12 +(number) + + +confidenceScore +0.94 +(number) + + +imageUrl +"" +(string) + + +pointsEarned +20 +(number) + + +timestamp +February 19, 2026 at 2:08:15 AM UTC+8 +(timestamp) + + +wasteType +"Plastic Bottle" +(string) + + +weekId +"2026-W07" \ No newline at end of file From ad1444a0b881cef485ab4b399e2728e79e061506 Mon Sep 17 00:00:00 2001 From: SNFASA Date: Wed, 25 Feb 2026 01:39:00 +0800 Subject: [PATCH 2/6] history page --- lib/core/widgets/impact_row.dart | 2 +- lib/core/widgets/smart_result_modal.dart | 2 +- lib/features/auth/ui/register_screen.dart | 8 +- lib/features/auth/ui/welcome_screen.dart | 14 +- .../controllers/analytics_controller.dart | 9 +- .../home/controllers/history_controller.dart | 25 ++ lib/features/home/models/user_model.dart | 2 +- .../home/repositories/history_repository.dart | 27 ++ lib/features/home/ui/analytics_screen.dart | 46 +-- lib/features/home/ui/camera_screen.dart | 8 +- lib/features/home/ui/history_screen.dart | 270 ++++++++++++++++++ lib/features/home/ui/home_screen.dart | 10 +- lib/features/home/ui/scoreboard_screen.dart | 14 +- lib/features/home/ui/template_screen.dart | 10 +- lib/features/home/ui/text.txt | 12 +- pubspec.lock | 8 + pubspec.yaml | 1 + 17 files changed, 402 insertions(+), 66 deletions(-) create mode 100644 lib/features/home/controllers/history_controller.dart create mode 100644 lib/features/home/repositories/history_repository.dart create mode 100644 lib/features/home/ui/history_screen.dart diff --git a/lib/core/widgets/impact_row.dart b/lib/core/widgets/impact_row.dart index 637504b..287e79b 100644 --- a/lib/core/widgets/impact_row.dart +++ b/lib/core/widgets/impact_row.dart @@ -24,7 +24,7 @@ class ImpactRow extends StatelessWidget { LinearProgressIndicator( value: value, color: color, - backgroundColor: color.withOpacity(0.1), + backgroundColor: color.withValues(alpha: 0.1), minHeight: 12, borderRadius: BorderRadius.circular(10), ), diff --git a/lib/core/widgets/smart_result_modal.dart b/lib/core/widgets/smart_result_modal.dart index e1e323b..65b1d27 100644 --- a/lib/core/widgets/smart_result_modal.dart +++ b/lib/core/widgets/smart_result_modal.dart @@ -37,7 +37,7 @@ class SmartResultModal extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( - color: themeColor.withOpacity(0.1), + color: themeColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(20), border: Border.all(color: themeColor, width: 2), ), diff --git a/lib/features/auth/ui/register_screen.dart b/lib/features/auth/ui/register_screen.dart index 32251a6..7676c82 100644 --- a/lib/features/auth/ui/register_screen.dart +++ b/lib/features/auth/ui/register_screen.dart @@ -90,7 +90,7 @@ class _RegisterScreenState extends ConsumerState { color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10)), ], ), child: Padding( @@ -213,8 +213,8 @@ class _RegisterScreenState extends ConsumerState { ), child: Stack( children: [ - Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))), - Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), const Positioned( top: 80, left: 30, @@ -240,7 +240,7 @@ class _RegisterScreenState extends ConsumerState { labelText: label, prefixIcon: Icon(icon, color: Colors.green[700]), filled: true, - fillColor: Colors.green.withOpacity(0.05), + fillColor: Colors.green.withValues(alpha: 0.05), border: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none), focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(15), borderSide: const BorderSide(color: Colors.green, width: 2)), diff --git a/lib/features/auth/ui/welcome_screen.dart b/lib/features/auth/ui/welcome_screen.dart index 1af3495..79fdef1 100644 --- a/lib/features/auth/ui/welcome_screen.dart +++ b/lib/features/auth/ui/welcome_screen.dart @@ -65,7 +65,7 @@ class WelcomeScreen extends StatelessWidget { color: Colors.white, boxShadow: [ BoxShadow( - color: Colors.green.withOpacity(0.05), + color: Colors.green.withValues(alpha: 0.05), blurRadius: 20, offset: const Offset(0, -5), ), @@ -136,8 +136,8 @@ class WelcomeScreen extends StatelessWidget { child: Stack( clipBehavior: Clip.none, children: [ - Positioned(top: -50, right: -20, child: _circleDeco(150, Colors.white.withOpacity(0.08))), - Positioned(bottom: -30, left: -40, child: _circleDeco(120, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -20, child: _circleDeco(150, Colors.white.withValues(alpha: 0.08))), + Positioned(bottom: -30, left: -40, child: _circleDeco(120, Colors.white.withValues(alpha: 0.05))), Center( child: Column( @@ -149,7 +149,7 @@ class WelcomeScreen extends StatelessWidget { color: Colors.white, shape: BoxShape.circle, boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow(color: Colors.black.withValues(alpha: 0.1), blurRadius: 20, offset: const Offset(0, 10)), ], ), child: const Icon(Icons.eco_rounded, size: 50, color: Color(0xFF1B5E20)), @@ -163,7 +163,7 @@ class WelcomeScreen extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: Colors.white.withValues(alpha: 0.2), borderRadius: BorderRadius.circular(20), ), child: const Text( @@ -186,14 +186,14 @@ class WelcomeScreen extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 15, offset: const Offset(0, 8)), + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 15, offset: const Offset(0, 8)), ], ), child: Row( children: [ Container( padding: const EdgeInsets.all(12), - decoration: BoxDecoration(color: iconColor.withOpacity(0.1), shape: BoxShape.circle), + decoration: BoxDecoration(color: iconColor.withValues(alpha: 0.1), shape: BoxShape.circle), child: Icon(icon, color: iconColor, size: 28), ), const SizedBox(width: 16), diff --git a/lib/features/home/controllers/analytics_controller.dart b/lib/features/home/controllers/analytics_controller.dart index 83bb9a3..7e711ca 100644 --- a/lib/features/home/controllers/analytics_controller.dart +++ b/lib/features/home/controllers/analytics_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; -import '../models/user_model.dart'; // Use a simple enum for the filters enum AnalyticsFilter { week, month, allTime } @@ -30,9 +29,8 @@ class AnalyticsController extends AsyncNotifier> { // INSTANT: Fetch from Category Counter in User Document final doc = await FirebaseFirestore.instance.collection('users').doc(user.uid).get(); final data = doc.data()?['categoryCounts'] as Map? ?? {}; - - // Convert Map to Map for the UI bars - return data.map((key, value) => MapEntry(key, (value as num).toDouble())); + final Map rawData = Map.from(data); + return Map.from(rawData.map((key, value) => MapEntry(key, (value as num).toDouble()))); } else { // DYNAMIC: Query Scans sub-collection for specific range DateTime now = DateTime.now(); @@ -49,7 +47,8 @@ class AnalyticsController extends AsyncNotifier> { Map counts = {}; for (var doc in query.docs) { - final cat = doc.data()['category'] ?? 'General'; + final data = doc.data(); + final String cat = data['category']?.toString() ?? 'General'; counts[cat] = (counts[cat] ?? 0.0) + 1.0; } return counts; diff --git a/lib/features/home/controllers/history_controller.dart b/lib/features/home/controllers/history_controller.dart new file mode 100644 index 0000000..7d7fb0a --- /dev/null +++ b/lib/features/home/controllers/history_controller.dart @@ -0,0 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/scan_model.dart'; +import '../repositories/history_repository.dart'; + +final scanHistoryProvider = StreamNotifierProvider>(() { + return HistoryController(); +}); + +class HistoryController extends StreamNotifier> { + @override + Stream> build() { + return ref.read(historyRepositoryProvider).getScanHistoryStream(); + } + + // Logic for the Floating Card metric + int getTodayScanCount(List scans) { + final now = DateTime.now(); + return scans.where((scan) { + final date = scan.timestamp.toDate(); + return date.year == now.year && + date.month == now.month && + date.day == now.day; + }).length; + } +} \ No newline at end of file diff --git a/lib/features/home/models/user_model.dart b/lib/features/home/models/user_model.dart index 9a10c66..e0f8937 100644 --- a/lib/features/home/models/user_model.dart +++ b/lib/features/home/models/user_model.dart @@ -42,7 +42,7 @@ class UserModel { rankTier: json['rankTier'] ?? 'Bronze', streak: json['streak'] ?? 0, categoryCounts: Map.from(json['categoryCounts'] ?? {}), - nextMilestoneCo2: (json['nextMilestoneCo2'] ?? 20.0).toDouble(), + nextMilestoneCo2: (json['nextMilestoneCo2'] ?? 50.0).toDouble(), lastScanWeekId: json['lastScanWeekId'] ?? '', ); } diff --git a/lib/features/home/repositories/history_repository.dart b/lib/features/home/repositories/history_repository.dart new file mode 100644 index 0000000..ea2b301 --- /dev/null +++ b/lib/features/home/repositories/history_repository.dart @@ -0,0 +1,27 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../models/scan_model.dart'; + +final historyRepositoryProvider = Provider((ref) => HistoryRepository()); + +class HistoryRepository { + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final FirebaseAuth _auth = FirebaseAuth.instance; + + Stream> getScanHistoryStream() { + final uid = _auth.currentUser?.uid; + if (uid == null) return Stream.value([]); + + // Streams the specific user's scan sub-collection in real-time + return _firestore + .collection('users') + .doc(uid) + .collection('scans') + .orderBy('timestamp', descending: true) // Newest first + .snapshots() + .map((snapshot) { + return snapshot.docs.map((doc) => ScanModel.fromMap(doc.data())).toList(); + }); + } +} \ No newline at end of file diff --git a/lib/features/home/ui/analytics_screen.dart b/lib/features/home/ui/analytics_screen.dart index 107a798..eaa0e6f 100644 --- a/lib/features/home/ui/analytics_screen.dart +++ b/lib/features/home/ui/analytics_screen.dart @@ -98,8 +98,8 @@ class AnalyticsScreen extends ConsumerWidget { ), child: Stack( children: [ - Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))), - Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), Positioned( top: 60, left: 0, @@ -129,7 +129,7 @@ class AnalyticsScreen extends ConsumerWidget { color: Colors.white, borderRadius: BorderRadius.circular(25), boxShadow: [ - BoxShadow(color: Colors.green.withOpacity(0.2), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow(color: Colors.green.withValues(alpha: 0.2), blurRadius: 20, offset: const Offset(0, 10)), ], ), child: Row( @@ -187,26 +187,30 @@ class AnalyticsScreen extends ConsumerWidget { } Widget _buildChartList(Map data) { - if (data.isEmpty) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 20), - child: Text("No data for this period.", style: TextStyle(color: Colors.grey)), - ); - } - - final total = data.values.fold(0.0, (sum, val) => sum + val); - - return Column( - children: data.entries.map((e) { - return _buildImpactBar( - e.key, - e.value / total, - _getCategoryColor(e.key), - _getCategoryIcon(e.key), - ); - }).toList(), + // DEFECT FIX: If there is no data or total is zero, show a friendly message + final total = data.values.fold(0.0, (sum, val) => sum + val); + + if (data.isEmpty || total == 0) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Center( + child: Text("No recycling data found for this period.", + style: TextStyle(color: Colors.grey, fontWeight: FontWeight.w500)), + ), ); } + + return Column( + children: data.entries.map((e) { + return _buildImpactBar( + e.key, + e.value / total, // Now safe because total > 0 + _getCategoryColor(e.key), + _getCategoryIcon(e.key), + ); + }).toList(), + ); +} Widget _buildMilestoneCard(double current, double target) { double progress = (target > 0) ? (current / target).clamp(0.0, 1.0) : 0.0; diff --git a/lib/features/home/ui/camera_screen.dart b/lib/features/home/ui/camera_screen.dart index 933b7d4..bac842e 100644 --- a/lib/features/home/ui/camera_screen.dart +++ b/lib/features/home/ui/camera_screen.dart @@ -155,7 +155,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr // Focus Overlay ColorFiltered( colorFilter: ColorFilter.mode( - Colors.black.withOpacity(0.5), + Colors.black.withValues(alpha: 0.5), BlendMode.srcOut, ), child: Stack( @@ -199,9 +199,9 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr height: 4, decoration: BoxDecoration( gradient: LinearGradient( - colors: [Colors.green.withOpacity(0), Colors.green, Colors.green.withOpacity(0)], + colors: [Colors.green.withValues(alpha: 0), Colors.green, Colors.green.withValues(alpha: 0)], ), - boxShadow: [BoxShadow(color: Colors.green.withOpacity(0.6), blurRadius: 10, spreadRadius: 2)], + boxShadow: [BoxShadow(color: Colors.green.withValues(alpha: 0.6), blurRadius: 10, spreadRadius: 2)], ), ), ), @@ -218,7 +218,7 @@ class _CameraScreenState extends ConsumerState with SingleTickerPr width: 320, height: 320, decoration: BoxDecoration( - border: Border.all(color: Colors.white.withOpacity(0.3), width: 1), + border: Border.all(color: Colors.white.withValues(alpha: 0.3), width: 1), borderRadius: BorderRadius.circular(25), ), child: Column( diff --git a/lib/features/home/ui/history_screen.dart b/lib/features/home/ui/history_screen.dart new file mode 100644 index 0000000..77ab000 --- /dev/null +++ b/lib/features/home/ui/history_screen.dart @@ -0,0 +1,270 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import '../controllers/history_controller.dart'; +import '../controllers/user_controller.dart'; +import '../models/scan_model.dart'; + +class HistoryScreen extends ConsumerWidget { + const HistoryScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final historyAsync = ref.watch(scanHistoryProvider); + final uid = FirebaseAuth.instance.currentUser!.uid; + final userAsync = ref.watch(userControllerProvider(uid)); + + return Scaffold( + backgroundColor: const Color(0xFFF4F9F5), + body: userAsync.when( + loading: () => const Center(child: CircularProgressIndicator(color: Colors.green)), + error: (err, _) => Center(child: Text("User Data Error: $err")), + data: (user) => historyAsync.when( + loading: () => const Center(child: CircularProgressIndicator(color: Colors.green)), + error: (err, _) => Center(child: Text("History Error: $err")), + data: (scans) { + final todayCount = ref.read(scanHistoryProvider.notifier).getTodayScanCount(scans); + + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1000), + child: SingleChildScrollView( + child: Column( + children: [ + // Updated Header Section with proper parameters + _buildHeaderSection( + user.ecoPoints, + user.totalScans, + MediaQuery.of(context).size.width, + user.rankTier, + todayCount + ), + const SizedBox(height: 80), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "Scanning History", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 15), + + // The Real-time List + if (scans.isEmpty) + const Padding( + padding: EdgeInsets.symmetric(vertical: 40), + child: Center(child: Text("No scans recorded yet.", style: TextStyle(color: Colors.grey))), + ) + else + ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: scans.length, + itemBuilder: (context, index) => _buildHistoryItem(context, scans[index]), + ), + + const SizedBox(height: 100), + ], + ), + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + } + + Widget _buildHistoryItem(BuildContext context, ScanModel scan) { + final dateStr = DateFormat('dd MMM yyyy, hh:mm a').format(scan.timestamp.toDate()); + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: InkWell( + onTap: () => _showScanDetails(context, scan), + borderRadius: BorderRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(15), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.03), + blurRadius: 10, + offset: const Offset(0, 4) + ) + ], + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + shape: BoxShape.circle + ), + child: const Icon(Icons.history_edu_rounded, color: Colors.green), + ), + const SizedBox(width: 15), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(scan.wasteType, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), + Text(dateStr, style: const TextStyle(color: Colors.grey, fontSize: 13)), + ], + ), + ), + Text("+${scan.pointsEarned} pts", style: const TextStyle(color: Colors.green, fontWeight: FontWeight.bold)), + ], + ), + ), + ), + ); + } + + void _showScanDetails(BuildContext context, ScanModel scan) { + showDialog( + context: context, + builder: (context) => AlertDialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)), + title: Text(scan.wasteType.toUpperCase(), textAlign: TextAlign.center), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(15), + child: scan.imageUrl.isNotEmpty + ? Image.network(scan.imageUrl, height: 200, width: double.infinity, fit: BoxFit.cover) + : Container(height: 200, color: Colors.grey[200], child: const Icon(Icons.image)), + ), + const SizedBox(height: 20), + _popupRow("Category", scan.category), + _popupRow("CO2 Saved", "${scan.co2Saved} kg"), + _popupRow("Confidence", "${(scan.confidenceScore * 100).toInt()}%"), + ], + ), + ), + ); + } + + Widget _popupRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: const TextStyle(color: Colors.grey)), + Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), + ], + ), + ); + } + + Widget _buildHeaderSection(int points, int scans, double screenWidth, String rank, int count) { + return Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + // 🟩 The Green Gradient Header + Container( + height: 280, + width: double.infinity, + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Color(0xFF1B5E20), Color(0xFF4CAF50)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(40), + bottomRight: Radius.circular(40), + ), + ), + child: Stack( + children: [ + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withAlpha(25))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withAlpha(13))), + + Padding( + padding: const EdgeInsets.only(top: 80, left: 30, right: 30), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "History", + style: TextStyle(color: Colors.white70, fontSize: 16) + ), + const SizedBox(height: 5), + const Text( + "Your journey to a greener Earth 🌿", + style: TextStyle(color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold) + ), + const SizedBox(height: 15), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withAlpha(30), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + "Level: $rank", + style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold), + ), + ), + ], + ), + ), + ], + ), + ), + + // ☁️ The Overlapping Floating Card (Stats) + Positioned( + bottom: -50, + child: Container( + width: screenWidth > 600 ? 500 : 340, + padding: const EdgeInsets.all(25), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.green.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10) + ) + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.eco, color: Colors.green), + const SizedBox(width: 10), + Text( + "Today's Scans: $count", + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 18) + ), + ], + ), + ), + ), + ], + ); + } + + Widget _circleDeco(double size, Color color) { + return Container( + width: size, + height: size, + decoration: BoxDecoration(shape: BoxShape.circle, color: color), + ); + } +} \ No newline at end of file diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index d9dbd66..5c8333a 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -7,6 +7,7 @@ import 'scoreboard_screen.dart'; import 'camera_screen.dart'; import 'analytics_screen.dart'; import 'profiles_screen.dart'; +import 'history_screen.dart'; // Controller import '../controllers/user_controller.dart'; @@ -24,7 +25,7 @@ class _HomeScreenState extends ConsumerState { void _onItemTapped(int index) async { // Logout index = 5 - if (index == 5) { + if (index == 6) { final confirm = await showDialog( context: context, builder: (context) => AlertDialog( @@ -66,6 +67,7 @@ class _HomeScreenState extends ConsumerState { const ScoreboardScreen(), const CameraScreen(), const AnalyticsScreen(), + const HistoryScreen(), const ProfileScreen(), ]; @@ -86,6 +88,7 @@ class _HomeScreenState extends ConsumerState { NavigationDestination(icon: Icon(Icons.emoji_events_rounded), label: 'Ranks'), NavigationDestination(icon: Icon(Icons.camera_enhance_rounded), label: 'Scan'), NavigationDestination(icon: Icon(Icons.insights_rounded), label: 'Impact'), + NavigationDestination(icon: Icon(Icons.history_rounded), label: 'History'), NavigationDestination(icon: Icon(Icons.person_rounded), label: 'Profile'), ], ), @@ -108,6 +111,7 @@ class _HomeScreenState extends ConsumerState { NavigationRailDestination(icon: Icon(Icons.emoji_events_rounded), label: Text('Ranks')), NavigationRailDestination(icon: Icon(Icons.camera_enhance_rounded), label: Text('Scan')), NavigationRailDestination(icon: Icon(Icons.insights_rounded), label: Text('Impact')), + NavigationRailDestination(icon: Icon(Icons.history_rounded), label: Text('History')), NavigationRailDestination(icon: Icon(Icons.person_rounded), label: Text('Profile')), ], ), @@ -306,9 +310,7 @@ class DashboardTab extends ConsumerWidget { children: [ Expanded(child: _actionButton(Icons.qr_code_scanner, "Scan Now", Colors.blue, () => onSwitchTab(2))), const SizedBox(width: 15), - Expanded(child: _actionButton(Icons.history_rounded, "History", Colors.orange, () { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("History coming soon!"))); - })), + Expanded(child: _actionButton(Icons.history_rounded, "History", Colors.orange, () => onSwitchTab(4))), const SizedBox(width: 15), Expanded(child: _actionButton(Icons.map_rounded, "Centers", Colors.teal, () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("Maps coming soon!"))); diff --git a/lib/features/home/ui/scoreboard_screen.dart b/lib/features/home/ui/scoreboard_screen.dart index 7edbfa0..b42e448 100644 --- a/lib/features/home/ui/scoreboard_screen.dart +++ b/lib/features/home/ui/scoreboard_screen.dart @@ -70,7 +70,7 @@ class ScoreboardScreen extends ConsumerWidget { boxShadow: [ BoxShadow( color: - Colors.black.withOpacity(0.03), + Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4), ) @@ -180,12 +180,12 @@ class ScoreboardScreen extends ConsumerWidget { top: -50, right: -50, child: _circleDeco( - 150, Colors.white.withOpacity(0.1))), + 150, Colors.white.withValues(alpha: 0.1))), Positioned( top: 50, left: -20, child: _circleDeco( - 100, Colors.white.withOpacity(0.05))), + 100, Colors.white.withValues(alpha: 0.05))), const Positioned( top: 50, left: 0, @@ -222,7 +222,7 @@ class ScoreboardScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( - color: Colors.green.withOpacity(0.2), + color: Colors.green.withValues(alpha: 0.2), blurRadius: 20, offset: const Offset(0, 10)), ], @@ -268,7 +268,7 @@ class ScoreboardScreen extends ConsumerWidget { CircleAvatar( radius: rank == 1 ? 25 : 18, backgroundColor: - isMe ? Colors.green : color.withOpacity(0.2), + isMe ? Colors.green : color.withValues(alpha: 0.2), backgroundImage: user.profileurl.isNotEmpty ? NetworkImage(user.profileurl) : null, @@ -306,8 +306,8 @@ class ScoreboardScreen extends ConsumerWidget { const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: isMe - ? Colors.green.withOpacity(0.3) - : color.withOpacity(0.3), + ? Colors.green.withValues(alpha: 0.3) + : color.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(5)), alignment: Alignment.center, child: Text( diff --git a/lib/features/home/ui/template_screen.dart b/lib/features/home/ui/template_screen.dart index de4d637..f726e99 100644 --- a/lib/features/home/ui/template_screen.dart +++ b/lib/features/home/ui/template_screen.dart @@ -75,8 +75,8 @@ class TemplateScreen extends ConsumerWidget { child: Stack( children: [ // 🫧 Decorative Circles - Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))), - Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), // 🅰️ Header Text const Padding( @@ -105,7 +105,7 @@ class TemplateScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( - color: Colors.green.withOpacity(0.2), // ❇️ Soft Green Glow + color: Colors.green.withValues(alpha: 0.2), // ❇️ Soft Green Glow blurRadius: 20, offset: const Offset(0, 10), ), @@ -129,7 +129,7 @@ class TemplateScreen extends ConsumerWidget { borderRadius: BorderRadius.circular(20), // 🟢 20px Radius boxShadow: [ BoxShadow( - color: Colors.black.withOpacity(0.03), // Super subtle shadow + color: Colors.black.withValues(alpha: 0.03), // Super subtle shadow blurRadius: 10, offset: const Offset(0, 4), ), @@ -141,7 +141,7 @@ class TemplateScreen extends ConsumerWidget { Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.blue.withOpacity(0.1), // 10% Opacity Background + color: Colors.blue.withValues(alpha: 0.1), // 10% Opacity Background shape: BoxShape.circle, ), child: const Icon(Icons.eco_rounded, color: Colors.blue, size: 28), diff --git a/lib/features/home/ui/text.txt b/lib/features/home/ui/text.txt index 931ab15..32d5867 100644 --- a/lib/features/home/ui/text.txt +++ b/lib/features/home/ui/text.txt @@ -54,7 +54,7 @@ class ScoreboardScreen extends StatelessWidget { borderRadius: BorderRadius.circular(20), border: isMe ? Border.all(color: Colors.green, width: 2) : null, boxShadow: [ - BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 10, offset: const Offset(0, 4)) + BoxShadow(color: Colors.black.withValues(alpha: 0.03), blurRadius: 10, offset: const Offset(0, 4)) ], ), child: ListTile( @@ -111,8 +111,8 @@ class ScoreboardScreen extends StatelessWidget { ), child: Stack( children: [ - Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withOpacity(0.1))), - Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withOpacity(0.05))), + Positioned(top: -50, right: -50, child: _circleDeco(150, Colors.white.withValues(alpha: 0.1))), + Positioned(top: 50, left: -20, child: _circleDeco(100, Colors.white.withValues(alpha: 0.05))), // ⚠️ FIXED: Moved text higher up (top: 50) so the card doesn't cover it const Positioned( @@ -143,7 +143,7 @@ class ScoreboardScreen extends StatelessWidget { color: Colors.white, borderRadius: BorderRadius.circular(25), boxShadow: [ - BoxShadow(color: Colors.green.withOpacity(0.2), blurRadius: 20, offset: const Offset(0, 10)), + BoxShadow(color: Colors.green.withValues(alpha: 0.2), blurRadius: 20, offset: const Offset(0, 10)), ], ), child: Row( @@ -169,7 +169,7 @@ class ScoreboardScreen extends StatelessWidget { if (rank == 1) const Icon(Icons.emoji_events, color: Colors.amber, size: 24), CircleAvatar( radius: rank == 1 ? 25 : 18, - backgroundColor: color.withOpacity(0.2), + backgroundColor: color.withValues(alpha: 0.2), child: Text(user['avatar'], style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12, color: Colors.black87)), ), const SizedBox(height: 5), @@ -181,7 +181,7 @@ class ScoreboardScreen extends StatelessWidget { height: rank == 1 ? 60 : (rank == 2 ? 40 : 30), width: double.infinity, margin: const EdgeInsets.symmetric(horizontal: 4), - decoration: BoxDecoration(color: color.withOpacity(0.3), borderRadius: BorderRadius.circular(5)), + decoration: BoxDecoration(color: color.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(5)), alignment: Alignment.center, child: Text("$rank", style: TextStyle(fontWeight: FontWeight.bold, color: color, fontSize: 18)), ) diff --git a/pubspec.lock b/pubspec.lock index a450860..c6e8400 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -512,6 +512,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 5ae6d52..658828f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,6 +24,7 @@ dependencies: path: ^1.9.1 permission_handler: ^12.0.1 image_picker: ^1.1.1 + intl: ^0.20.2 dev_dependencies: From 39d52c632408371c03270e24eb345bbab27e87e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:48:58 +0000 Subject: [PATCH 3/6] Initial plan From 6d2b5c4c09cd5286e88f9089237982b6f7419bd4 Mon Sep 17 00:00:00 2001 From: SNFASA Date: Wed, 25 Feb 2026 02:45:53 +0800 Subject: [PATCH 4/6] centers page --- android/app/src/main/AndroidManifest.xml | 4 + ios/Runner/AppDelegate.swift | 3 +- .../home/controllers/centers_controller.dart | 44 ++++ ...model.dart => recycleingcenter_model.dart} | 0 .../home/repositories/centers_repository.dart | 18 ++ lib/features/home/ui/centers_screen.dart | 125 +++++++++++ lib/features/home/ui/home_screen.dart | 10 +- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + pubspec.lock | 208 ++++++++++++++++++ pubspec.yaml | 4 + .../flutter/generated_plugin_registrant.cc | 6 + windows/flutter/generated_plugins.cmake | 2 + 14 files changed, 428 insertions(+), 5 deletions(-) create mode 100644 lib/features/home/controllers/centers_controller.dart rename lib/features/home/models/{recycleingcender_model.dart => recycleingcenter_model.dart} (100%) create mode 100644 lib/features/home/repositories/centers_repository.dart create mode 100644 lib/features/home/ui/centers_screen.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 97e9da7..9cb1449 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ + + +