This commit is contained in:
Rishad7594
2026-04-08 19:18:33 +05:30
parent aefb381ed3
commit bbef5b376d
6 changed files with 62 additions and 23 deletions

Binary file not shown.

View File

@@ -109,21 +109,24 @@ class ApiClient {
bool requiresAuth = true, bool requiresAuth = true,
}) async { }) async {
// build final query params including auth if needed // build final query params including auth if needed
final Map<String, dynamic> finalParams = {}; final originalUri = Uri.parse(url);
final queryParams = <String, String>{...originalUri.queryParameters};
if (requiresAuth) { if (requiresAuth) {
final token = await TokenStorage.getToken(); final token = await TokenStorage.getToken();
final username = await TokenStorage.getUsername(); final username = await TokenStorage.getUsername();
if (token != null && username != null) { if (token != null && username != null) {
finalParams['token'] = token; queryParams['token'] = token;
finalParams['username'] = username; queryParams['username'] = username;
} }
// Guest mode: proceed without token — let backend decide // Guest mode: proceed without token — let backend decide
} }
if (params != null) finalParams.addAll(params); if (params != null) {
queryParams.addAll(params.map((k, v) => MapEntry(k, v?.toString() ?? '')));
}
final uri = Uri.parse(url).replace(queryParameters: finalParams.map((k, v) => MapEntry(k, v?.toString()))); final uri = originalUri.replace(queryParameters: queryParams);
late http.Response response; late http.Response response;
try { try {
@@ -133,7 +136,7 @@ class ApiClient {
throw Exception('Network error: $e'); throw Exception('Network error: $e');
} }
return _handleResponse(url, response, finalParams); return _handleResponse(url, response, queryParams);
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -1,6 +1,8 @@
// lib/features/gamification/models/gamification_models.dart // lib/features/gamification/models/gamification_models.dart
// Data models matching TechDocs v2 DB schema for the Contributor Module. // Data models matching TechDocs v2 DB schema for the Contributor Module.
import 'package:flutter/foundation.dart';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Tier enum — matches PRD v3 §4.4 thresholds (based on Lifetime EP) // Tier enum — matches PRD v3 §4.4 thresholds (based on Lifetime EP)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -68,13 +70,21 @@ int tierStartEp(ContributorTier tier) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
class UserGamificationProfile { class UserGamificationProfile {
final String userId; final String userId;
final int lifetimeEp; // Never resets. Used for tiers + leaderboard. final String username;
final int currentEp; // Liquid EP accumulated this month. final String? avatarUrl;
final int currentRp; // Spendable Reward Points. final String? district;
final String? eventifyId;
final int lifetimeEp; // Never resets. Used for tiers + leaderboard.
final int currentEp; // Liquid EP accumulated this month.
final int currentRp; // Spendable Reward Points.
final ContributorTier tier; final ContributorTier tier;
const UserGamificationProfile({ const UserGamificationProfile({
required this.userId, required this.userId,
required this.username,
this.avatarUrl,
this.district,
this.eventifyId,
required this.lifetimeEp, required this.lifetimeEp,
required this.currentEp, required this.currentEp,
required this.currentRp, required this.currentRp,
@@ -82,12 +92,17 @@ class UserGamificationProfile {
}); });
factory UserGamificationProfile.fromJson(Map<String, dynamic> json) { factory UserGamificationProfile.fromJson(Map<String, dynamic> json) {
final ep = (json['lifetime_ep'] as int?) ?? 0; debugPrint('Mapping UserGamificationProfile from JSON: $json');
final ep = (json['lifetime_ep'] as int?) ?? (json['points'] as int?) ?? (json['total_points'] as int?) ?? 0;
return UserGamificationProfile( return UserGamificationProfile(
userId: json['user_id'] as String? ?? '', userId: (json['user_id'] ?? json['email'] ?? json['userId'] ?? '').toString(),
username: (json['username'] ?? json['name'] ?? json['full_name'] ?? json['display_name'] ?? '').toString(),
avatarUrl: json['profile_image'] as String? ?? json['avatar_url'] as String? ?? json['profile_pic'] as String?,
district: json['district'] as String? ?? json['location'] as String?,
eventifyId: (json['eventify_id'] ?? json['eventifyId'] ?? json['id'] ?? '').toString(),
lifetimeEp: ep, lifetimeEp: ep,
currentEp: (json['current_ep'] as int?) ?? 0, currentEp: (json['current_ep'] as int?) ?? (json['monthly_points'] as int?) ?? (json['points_this_month'] as int?) ?? 0,
currentRp: (json['current_rp'] as int?) ?? 0, currentRp: (json['current_rp'] as int?) ?? (json['reward_points'] as int?) ?? (json['rp'] as int?) ?? 0,
tier: tierFromEp(ep), tier: tierFromEp(ep),
); );
} }

View File

@@ -36,8 +36,10 @@ class GamificationProvider extends ChangeNotifier {
// Load everything at once (called when ContributeScreen or ProfileScreen mounts) // Load everything at once (called when ContributeScreen or ProfileScreen mounts)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
Future<void> loadAll({bool force = false}) async { Future<void> loadAll({bool force = false}) async {
// Skip if recently loaded (within 2 minutes) unless forced debugPrint('GamificationProvider.loadAll(force: $force) called');
if (!force && _lastLoadTime != null && DateTime.now().difference(_lastLoadTime!) < _loadTtl) { // 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; return;
} }
@@ -46,10 +48,20 @@ class GamificationProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
try { try {
debugPrint('GamificationProvider: Requesting dashboard, leaderboard, etc...');
final results = await Future.wait([ final results = await Future.wait([
_service.getDashboard().catchError((e) { _service.getDashboard().catchError((e) {
debugPrint('Dashboard error: $e'); debugPrint('Dashboard error: $e');
return const DashboardResponse(profile: UserGamificationProfile(userId: '', lifetimeEp: 0, currentEp: 0, currentRp: 0, tier: ContributorTier.BRONZE)); return const DashboardResponse(
profile: UserGamificationProfile(
userId: '',
username: '',
lifetimeEp: 0,
currentEp: 0,
currentRp: 0,
tier: ContributorTier.BRONZE,
),
);
}), }),
_service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod).catchError((e) { _service.getLeaderboard(district: leaderboardDistrict, timePeriod: leaderboardTimePeriod).catchError((e) {
debugPrint('Leaderboard error: $e'); debugPrint('Leaderboard error: $e');
@@ -179,6 +191,10 @@ class GamificationProvider extends ChangeNotifier {
if (profile != null) { if (profile != null) {
profile = UserGamificationProfile( profile = UserGamificationProfile(
userId: profile!.userId, userId: profile!.userId,
username: profile!.username,
avatarUrl: profile!.avatarUrl,
district: profile!.district,
eventifyId: profile!.eventifyId,
lifetimeEp: profile!.lifetimeEp, lifetimeEp: profile!.lifetimeEp,
currentEp: profile!.currentEp, currentEp: profile!.currentEp,
currentRp: profile!.currentRp - item.rpCost, currentRp: profile!.currentRp - item.rpCost,
@@ -195,6 +211,10 @@ class GamificationProvider extends ChangeNotifier {
if (profile != null) { if (profile != null) {
profile = UserGamificationProfile( profile = UserGamificationProfile(
userId: profile!.userId, userId: profile!.userId,
username: profile!.username,
avatarUrl: profile!.avatarUrl,
district: profile!.district,
eventifyId: profile!.eventifyId,
lifetimeEp: profile!.lifetimeEp, lifetimeEp: profile!.lifetimeEp,
currentEp: profile!.currentEp, currentEp: profile!.currentEp,
currentRp: profile!.currentRp + item.rpCost, currentRp: profile!.currentRp + item.rpCost,

View File

@@ -1785,7 +1785,7 @@ class _ContributeScreenState extends State<ContributeScreen>
try { try {
final data = <String, dynamic>{ final data = <String, dynamic>{
'title': _titleCtl.text.trim(), 'event_name': _titleCtl.text.trim(),
'category': _selectedCategory, 'category': _selectedCategory,
'district': _selectedDistrict, 'district': _selectedDistrict,
'date': _selectedDate!.toIso8601String(), 'date': _selectedDate!.toIso8601String(),

View File

@@ -111,7 +111,7 @@ class _ProfileScreenState extends State<ProfileScreen>
// Load gamification data for profile EP cards // Load gamification data for profile EP cards
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) context.read<GamificationProvider>().loadAll(); if (mounted) context.read<GamificationProvider>().loadAll(force: true);
}); });
} }
@@ -829,7 +829,7 @@ class _ProfileScreenState extends State<ProfileScreen>
// Username // Username
Text( Text(
_username, (p?.username.isNotEmpty == true) ? p!.username : _username,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 24, fontSize: 24,
@@ -840,10 +840,11 @@ class _ProfileScreenState extends State<ProfileScreen>
const SizedBox(height: 12), const SizedBox(height: 12),
// Eventify ID Badge // Eventify ID Badge
if (_eventifyId != null) if ((p?.eventifyId ?? _eventifyId) != null)
GestureDetector( GestureDetector(
onTap: () { onTap: () {
Clipboard.setData(ClipboardData(text: _eventifyId!)); final id = p?.eventifyId ?? _eventifyId!;
Clipboard.setData(ClipboardData(text: id));
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('ID copied to clipboard')), const SnackBar(content: Text('ID copied to clipboard')),
); );
@@ -861,7 +862,7 @@ class _ProfileScreenState extends State<ProfileScreen>
const Icon(Icons.copy, size: 14, color: Colors.white70), const Icon(Icons.copy, size: 14, color: Colors.white70),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
_eventifyId!, p?.eventifyId ?? _eventifyId!,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 13, fontSize: 13,
@@ -882,7 +883,7 @@ class _ProfileScreenState extends State<ProfileScreen>
const Icon(Icons.location_on_outlined, color: Colors.white, size: 18), const Icon(Icons.location_on_outlined, color: Colors.white, size: 18),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
_district ?? 'No district selected', p?.district ?? _district ?? 'No district selected',
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400), style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w400),
), ),
], ],