// lib/features/gamification/providers/gamification_provider.dart import 'package:flutter/foundation.dart'; import '../../../core/utils/error_utils.dart'; import '../models/gamification_models.dart'; import '../services/gamification_service.dart'; import '../../events/services/events_service.dart'; import '../../events/models/event_models.dart'; class GamificationProvider extends ChangeNotifier { final GamificationService _service = GamificationService(); // State UserGamificationProfile? profile; List leaderboard = []; List shopItems = []; List achievements = GamificationService.defaultBadges; // Initialize with defaults List submissions = []; CurrentUserStats? currentUserStats; int totalParticipants = 0; List eventCategories = []; // Leaderboard filters — matches web version String leaderboardDistrict = 'Overall Kerala'; String leaderboardTimePeriod = 'all_time'; // 'all_time' | 'this_month' bool isLoading = false; bool isLeaderboardLoading = false; String? error; // TTL guard — prevents redundant API calls from multiple screens DateTime? _lastLoadTime; static const _loadTtl = Duration(minutes: 2); // --------------------------------------------------------------------------- // Load everything at once (called when ContributeScreen or ProfileScreen mounts) // --------------------------------------------------------------------------- Future loadAll({bool force = false}) async { // Skip if recently loaded (within 2 minutes) unless forced if (!force && _lastLoadTime != null && DateTime.now().difference(_lastLoadTime!) < _loadTtl) { return; } isLoading = true; error = null; notifyListeners(); try { final results = await Future.wait([ _service.getDashboard().catchError((e) { debugPrint('Dashboard error: $e'); return const DashboardResponse(profile: UserGamificationProfile(userId: '', lifetimeEp: 0, currentEp: 0, currentRp: 0, tier: ContributorTier.BRONZE)); }), _service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod).catchError((e) { debugPrint('Leaderboard error: $e'); return const LeaderboardResponse(entries: []); }), _service.getShopItems().catchError((e) { debugPrint('Shop error: $e'); return []; }), _service.getAchievements().catchError((e) { debugPrint('Achievements error: $e'); return []; }), EventsService().getEventTypes().catchError((e) { debugPrint('EventTypes error: $e'); return []; }), ]); final dashboard = results[0] as DashboardResponse; profile = dashboard.profile; submissions = dashboard.submissions; final lbResponse = results[1] as LeaderboardResponse; leaderboard = _filterAndReRank(lbResponse.entries, leaderboardDistrict, leaderboardTimePeriod); currentUserStats = lbResponse.currentUser; totalParticipants = lbResponse.totalParticipants; shopItems = results[2] as List; // Prefer achievements from dashboard API; fall back to fetched or existing defaults final dashAchievements = dashboard.achievements; final fetchedAchievements = results[3] as List; if (dashAchievements.isNotEmpty) { achievements = dashAchievements; } else if (fetchedAchievements.isNotEmpty) { achievements = fetchedAchievements; } final eventTypes = results[4] as List; if (eventTypes.isNotEmpty) { eventCategories = eventTypes.map((e) => e.name).toList(); } // Otherwise, keep current defaults _lastLoadTime = DateTime.now(); } catch (e) { error = userFriendlyError(e); } finally { isLoading = false; notifyListeners(); } } // --------------------------------------------------------------------------- // Load leaderboard independently (decoupled from loadAll) // --------------------------------------------------------------------------- Future loadLeaderboard() async { isLeaderboardLoading = true; notifyListeners(); try { final response = await _service.getLeaderboard( district: leaderboardDistrict, timePeriod: leaderboardTimePeriod, ); leaderboard = response.entries; currentUserStats = response.currentUser; totalParticipants = response.totalParticipants; } catch (e) { error = userFriendlyError(e); } finally { isLeaderboardLoading = false; notifyListeners(); } } // --------------------------------------------------------------------------- // Change district filter // --------------------------------------------------------------------------- Future setDistrict(String district) async { if (leaderboardDistrict == district) return; leaderboardDistrict = district; isLeaderboardLoading = true; notifyListeners(); try { final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod); leaderboard = _filterAndReRank(response.entries, district, leaderboardTimePeriod); currentUserStats = response.currentUser; totalParticipants = response.totalParticipants; } catch (e) { error = userFriendlyError(e); } finally { isLeaderboardLoading = false; } notifyListeners(); } // --------------------------------------------------------------------------- // Change time period filter // --------------------------------------------------------------------------- Future setTimePeriod(String period) async { if (leaderboardTimePeriod == period) return; leaderboardTimePeriod = period; isLeaderboardLoading = true; notifyListeners(); try { final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period); leaderboard = _filterAndReRank(response.entries, leaderboardDistrict, period); currentUserStats = response.currentUser; totalParticipants = response.totalParticipants; } catch (e) { error = userFriendlyError(e); } finally { isLeaderboardLoading = false; } notifyListeners(); } // --------------------------------------------------------------------------- // Redeem a shop item — deducts RP locally optimistically, returns voucher code // --------------------------------------------------------------------------- Future redeemItem(String itemId) async { final item = shopItems.firstWhere((s) => s.id == itemId); // Optimistically deduct RP if (profile != null) { profile = UserGamificationProfile( userId: profile!.userId, lifetimeEp: profile!.lifetimeEp, currentEp: profile!.currentEp, currentRp: profile!.currentRp - item.rpCost, tier: profile!.tier, ); notifyListeners(); } try { final record = await _service.redeemItem(itemId); return record.voucherCode; } catch (e) { // Rollback on failure if (profile != null) { profile = UserGamificationProfile( userId: profile!.userId, lifetimeEp: profile!.lifetimeEp, currentEp: profile!.currentEp, currentRp: profile!.currentRp + item.rpCost, tier: profile!.tier, ); notifyListeners(); } rethrow; } } // --------------------------------------------------------------------------- // Submit a contribution // --------------------------------------------------------------------------- Future submitContribution(Map data) async { await _service.submitContribution(data); } // --------------------------------------------------------------------------- // Helper: Filter by district and re-rank results locally. // This is a fallback in case the backend returns a global list for a district-specific query. // --------------------------------------------------------------------------- List _filterAndReRank(List entries, String district, String period) { if (entries.isEmpty) return []; List result = entries; if (district != 'Overall Kerala') { // Case-insensitive filtering to be robust result = entries.where((e) => e.district?.toLowerCase() == district.toLowerCase()).toList(); } // Sort based on period if (period == 'this_month') { result.sort((a, b) => b.monthlyPoints.compareTo(a.monthlyPoints)); } else { result.sort((a, b) => b.lifetimeEp.compareTo(a.lifetimeEp)); } // Assign new ranks based on local sort order return List.generate(result.length, (i) { final e = result[i]; return LeaderboardEntry( rank: i + 1, username: e.username, avatarUrl: e.avatarUrl, lifetimeEp: e.lifetimeEp, monthlyPoints: e.monthlyPoints, tier: e.tier, eventsCount: e.eventsCount, isCurrentUser: e.isCurrentUser, district: e.district, ); }); } }