Files
Eventify-frontend/lib/features/gamification/providers/gamification_provider.dart
Rishad7594 bbef5b376d ...
2026-04-08 19:25:43 +05:30

273 lines
9.8 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';
import '../../events/services/events_service.dart';
import '../../events/models/event_models.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;
List<String> 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<void> loadAll({bool force = false}) async {
debugPrint('GamificationProvider.loadAll(force: $force) called');
// Skip if recently loaded (within 2 minutes) unless forced or profile is null
if (!force && profile != null && _lastLoadTime != null && DateTime.now().difference(_lastLoadTime!) < _loadTtl) {
debugPrint('GamificationProvider.loadAll skipped due to TTL');
return;
}
isLoading = true;
error = null;
notifyListeners();
try {
debugPrint('GamificationProvider: Requesting dashboard, leaderboard, etc...');
final results = await Future.wait([
_service.getDashboard().catchError((e) {
debugPrint('Dashboard error: $e');
return const DashboardResponse(
profile: UserGamificationProfile(
userId: '',
username: '',
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 <ShopItem>[];
}),
_service.getAchievements().catchError((e) {
debugPrint('Achievements error: $e');
return <AchievementBadge>[];
}),
EventsService().getEventTypes().catchError((e) {
debugPrint('EventTypes error: $e');
return <EventTypeModel>[];
}),
]);
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<ShopItem>;
// Prefer achievements from dashboard API; fall back to fetched or existing defaults
final dashAchievements = dashboard.achievements;
final fetchedAchievements = results[3] as List<AchievementBadge>;
if (dashAchievements.isNotEmpty) {
achievements = dashAchievements;
} else if (fetchedAchievements.isNotEmpty) {
achievements = fetchedAchievements;
}
final eventTypes = results[4] as List<EventTypeModel>;
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<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 = _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<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 = _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<String> redeemItem(String itemId) async {
final item = shopItems.firstWhere((s) => s.id == itemId);
// Optimistically deduct RP
if (profile != null) {
profile = UserGamificationProfile(
userId: profile!.userId,
username: profile!.username,
avatarUrl: profile!.avatarUrl,
district: profile!.district,
eventifyId: profile!.eventifyId,
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,
username: profile!.username,
avatarUrl: profile!.avatarUrl,
district: profile!.district,
eventifyId: profile!.eventifyId,
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);
}
// ---------------------------------------------------------------------------
// 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<LeaderboardEntry> _filterAndReRank(List<LeaderboardEntry> entries, String district, String period) {
if (entries.isEmpty) return [];
List<LeaderboardEntry> 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,
);
});
}
}