...
This commit is contained in:
Binary file not shown.
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user