- Add isLeaderboardLoading flag separate from isLoading - Add loadLeaderboard() method that fires independently of loadAll TTL - Remove leaderboard from loadAll() Future.wait (failures in dashboard/shop no longer silently zero-out leaderboard data) - setDistrict / setTimePeriod now use isLeaderboardLoading - contribute_screen calls loadLeaderboard() alongside loadAll() on mount Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
186 lines
6.3 KiB
Dart
186 lines
6.3 KiB
Dart
// 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';
|
|
|
|
class GamificationProvider extends ChangeNotifier {
|
|
final GamificationService _service = GamificationService();
|
|
|
|
// State
|
|
UserGamificationProfile? profile;
|
|
List<LeaderboardEntry> leaderboard = [];
|
|
List<ShopItem> shopItems = [];
|
|
List<AchievementBadge> achievements = GamificationService.defaultBadges; // Initialize with defaults
|
|
List<SubmissionModel> submissions = [];
|
|
CurrentUserStats? currentUserStats;
|
|
int totalParticipants = 0;
|
|
|
|
// 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<void> 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(),
|
|
_service.getShopItems(),
|
|
_service.getAchievements(),
|
|
]);
|
|
|
|
final dashboard = results[0] as DashboardResponse;
|
|
profile = dashboard.profile;
|
|
submissions = dashboard.submissions;
|
|
|
|
shopItems = results[1] as List<ShopItem>;
|
|
|
|
// Prefer achievements from dashboard API; fall back to fetched or existing defaults
|
|
final dashAchievements = dashboard.achievements;
|
|
final fetchedAchievements = results[2] as List<AchievementBadge>;
|
|
|
|
if (dashAchievements.isNotEmpty) {
|
|
achievements = dashAchievements;
|
|
} else if (fetchedAchievements.isNotEmpty) {
|
|
achievements = fetchedAchievements;
|
|
}
|
|
// Otherwise, keep current defaults
|
|
|
|
_lastLoadTime = DateTime.now();
|
|
} catch (e) {
|
|
error = userFriendlyError(e);
|
|
} finally {
|
|
isLoading = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Load leaderboard independently (decoupled from loadAll)
|
|
// ---------------------------------------------------------------------------
|
|
Future<void> 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<void> setDistrict(String district) async {
|
|
if (leaderboardDistrict == district) return;
|
|
leaderboardDistrict = district;
|
|
isLeaderboardLoading = true;
|
|
notifyListeners();
|
|
try {
|
|
final response = await _service.getLeaderboard(district: district, timePeriod: leaderboardTimePeriod);
|
|
leaderboard = response.entries;
|
|
currentUserStats = response.currentUser;
|
|
totalParticipants = response.totalParticipants;
|
|
} catch (e) {
|
|
error = userFriendlyError(e);
|
|
} finally {
|
|
isLeaderboardLoading = false;
|
|
}
|
|
notifyListeners();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Change time period filter
|
|
// ---------------------------------------------------------------------------
|
|
Future<void> setTimePeriod(String period) async {
|
|
if (leaderboardTimePeriod == period) return;
|
|
leaderboardTimePeriod = period;
|
|
isLeaderboardLoading = true;
|
|
notifyListeners();
|
|
try {
|
|
final response = await _service.getLeaderboard(district: leaderboardDistrict, timePeriod: period);
|
|
leaderboard = response.entries;
|
|
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<String> 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<void> submitContribution(Map<String, dynamic> data) async {
|
|
await _service.submitContribution(data);
|
|
}
|
|
}
|