- ApiClient.uploadFile() — multipart POST to /v1/upload/file (60s timeout)
- ApiEndpoints.uploadFile — points to Node.js upload endpoint
- GamificationService.submitContribution() now uploads each picked image
to OneDrive via the server upload pipeline, then passes the returned
{ fileId, url, ... } objects as `media` in the submission body
(replaces broken behaviour of sending local device paths as strings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
209 lines
8.6 KiB
Dart
209 lines
8.6 KiB
Dart
// lib/features/gamification/services/gamification_service.dart
|
|
//
|
|
// Real API service for the Contributor / Gamification module.
|
|
// Calls the Node.js gamification server at app.eventifyplus.com.
|
|
|
|
import '../../../core/api/api_client.dart';
|
|
import '../../../core/api/api_endpoints.dart';
|
|
import '../../../core/storage/token_storage.dart';
|
|
import '../models/gamification_models.dart';
|
|
|
|
class GamificationService {
|
|
final ApiClient _api = ApiClient();
|
|
|
|
/// Helper: get current user's email for API calls.
|
|
Future<String> _getUserEmail() async {
|
|
final email = await TokenStorage.getUsername();
|
|
return email ?? '';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Dashboard (profile + submissions)
|
|
// GET /v1/gamification/dashboard?user_id={email}
|
|
// ---------------------------------------------------------------------------
|
|
Future<DashboardResponse> getDashboard() async {
|
|
final email = await _getUserEmail();
|
|
final url = '${ApiEndpoints.gamificationDashboard}?user_id=${Uri.encodeComponent(email)}';
|
|
final res = await _api.get(url, requiresAuth: false);
|
|
|
|
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
|
final rawSubs = res['submissions'] as List? ?? [];
|
|
final rawAchievements = res['achievements'] as List? ?? [];
|
|
|
|
final submissions = rawSubs
|
|
.map((s) => SubmissionModel.fromJson(Map<String, dynamic>.from(s as Map)))
|
|
.toList();
|
|
|
|
final achievements = rawAchievements
|
|
.map((a) => AchievementBadge.fromJson(Map<String, dynamic>.from(a as Map)))
|
|
.toList();
|
|
|
|
return DashboardResponse(
|
|
profile: UserGamificationProfile.fromJson(profileJson),
|
|
submissions: submissions,
|
|
achievements: achievements,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public contributor profile (any user by userId / email)
|
|
// GET /v1/gamification/dashboard?user_id={userId}
|
|
// ---------------------------------------------------------------------------
|
|
Future<DashboardResponse> getDashboardForUser(String userId) async {
|
|
final url = '${ApiEndpoints.gamificationDashboard}?user_id=${Uri.encodeComponent(userId)}';
|
|
final res = await _api.get(url, requiresAuth: false);
|
|
|
|
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
|
final rawSubs = res['submissions'] as List? ?? [];
|
|
final rawAchievements = res['achievements'] as List? ?? [];
|
|
|
|
final submissions = rawSubs
|
|
.map((s) => SubmissionModel.fromJson(Map<String, dynamic>.from(s as Map)))
|
|
.toList();
|
|
|
|
final achievements = rawAchievements
|
|
.map((a) => AchievementBadge.fromJson(Map<String, dynamic>.from(a as Map)))
|
|
.toList();
|
|
|
|
return DashboardResponse(
|
|
profile: UserGamificationProfile.fromJson(profileJson),
|
|
submissions: submissions,
|
|
achievements: achievements,
|
|
);
|
|
}
|
|
|
|
/// Convenience — returns just the profile (backward-compatible with provider).
|
|
Future<UserGamificationProfile> getProfile() async {
|
|
final dashboard = await getDashboard();
|
|
return dashboard.profile;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Leaderboard
|
|
// GET /v1/gamification/leaderboard?period=all|month&district=X&user_id=Y&limit=50
|
|
// ---------------------------------------------------------------------------
|
|
Future<LeaderboardResponse> getLeaderboard({
|
|
required String district,
|
|
required String timePeriod,
|
|
}) async {
|
|
final email = await _getUserEmail();
|
|
|
|
// Map Flutter filter values to API params
|
|
final period = timePeriod == 'this_month' ? 'month' : 'all';
|
|
|
|
final params = <String, String>{
|
|
'period': period,
|
|
'user_id': email,
|
|
'limit': '50',
|
|
};
|
|
if (district != 'Overall Kerala') {
|
|
params['district'] = district;
|
|
}
|
|
|
|
final query = Uri(queryParameters: params).query;
|
|
final url = '${ApiEndpoints.leaderboard}?$query';
|
|
final res = await _api.get(url, requiresAuth: false);
|
|
|
|
final rawList = res['leaderboard'] as List? ?? [];
|
|
final entries = rawList
|
|
.map((e) => LeaderboardEntry.fromJson(Map<String, dynamic>.from(e as Map)))
|
|
.toList();
|
|
|
|
CurrentUserStats? currentUser;
|
|
if (res['currentUser'] != null && res['currentUser'] is Map) {
|
|
currentUser = CurrentUserStats.fromJson(
|
|
Map<String, dynamic>.from(res['currentUser'] as Map),
|
|
);
|
|
}
|
|
|
|
return LeaderboardResponse(
|
|
entries: entries,
|
|
currentUser: currentUser,
|
|
totalParticipants: (res['totalParticipants'] as num?)?.toInt() ?? entries.length,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shop Items
|
|
// GET /v1/shop/items
|
|
// ---------------------------------------------------------------------------
|
|
Future<List<ShopItem>> getShopItems() async {
|
|
final res = await _api.get(ApiEndpoints.shopItems, requiresAuth: false);
|
|
final rawItems = res['items'] as List? ?? [];
|
|
return rawItems
|
|
.map((e) => ShopItem.fromJson(Map<String, dynamic>.from(e as Map)))
|
|
.toList();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Redeem Item
|
|
// POST /v1/shop/redeem body: { user_id, item_id }
|
|
// ---------------------------------------------------------------------------
|
|
Future<RedemptionRecord> redeemItem(String itemId) async {
|
|
final email = await _getUserEmail();
|
|
final res = await _api.post(
|
|
ApiEndpoints.shopRedeem,
|
|
body: {'user_id': email, 'item_id': itemId},
|
|
requiresAuth: false,
|
|
);
|
|
final voucher = res['voucher'] as Map<String, dynamic>? ?? res;
|
|
return RedemptionRecord.fromJson(Map<String, dynamic>.from(voucher));
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Submit Contribution
|
|
// 1. Upload each image to /v1/upload/file → get back { url, fileId, ... }
|
|
// 2. POST /v1/gamification/submit-event with `media` (uploaded objects)
|
|
// ---------------------------------------------------------------------------
|
|
Future<void> submitContribution(Map<String, dynamic> data) async {
|
|
final email = await _getUserEmail();
|
|
|
|
// Upload images if present
|
|
final rawPaths = (data['images'] as List?)?.cast<String>() ?? [];
|
|
final List<Map<String, dynamic>> uploadedMedia = [];
|
|
|
|
for (final path in rawPaths) {
|
|
final result = await _api.uploadFile(ApiEndpoints.uploadFile, path);
|
|
uploadedMedia.add(result);
|
|
}
|
|
|
|
// Build submission body — use `media` (server canonical field)
|
|
final body = <String, dynamic>{
|
|
'user_id': email,
|
|
...Map.from(data)..remove('images'),
|
|
if (uploadedMedia.isNotEmpty) 'media': uploadedMedia,
|
|
};
|
|
|
|
await _api.post(
|
|
ApiEndpoints.contributeSubmit,
|
|
body: body,
|
|
requiresAuth: false,
|
|
);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Achievements — sourced from dashboard API `achievements` array.
|
|
// Falls back to default badges if API doesn't return achievements yet.
|
|
// ---------------------------------------------------------------------------
|
|
Future<List<AchievementBadge>> getAchievements() async {
|
|
try {
|
|
final dashboard = await getDashboard();
|
|
if (dashboard.achievements.isNotEmpty) return dashboard.achievements;
|
|
} catch (_) {
|
|
// Fall through to defaults
|
|
}
|
|
return defaultBadges;
|
|
}
|
|
|
|
static const defaultBadges = [
|
|
AchievementBadge(id: 'badge-01', title: 'Newcomer', description: 'First Event Posted', iconName: 'star', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-02', title: 'Contributor', description: '10th Event Posted within a month', iconName: 'crown', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-03', title: 'On Fire!', description: '3 Day Streak of logging in', iconName: 'fire', isUnlocked: false, progress: 0.67),
|
|
AchievementBadge(id: 'badge-04', title: 'Verified', description: 'Identity Verified successfully', iconName: 'verified', isUnlocked: true, progress: 1.0),
|
|
AchievementBadge(id: 'badge-05', title: 'Quality', description: '5 Star Event Rating received', iconName: 'star', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-06', title: 'Community', description: 'Referred 5 Friends to the platform', iconName: 'community', isUnlocked: false, progress: 0.4),
|
|
AchievementBadge(id: 'badge-07', title: 'Expert', description: 'Level 10 Reached in 3 months', iconName: 'expert', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-08', title: 'Precision', description: '100% Data Accuracy on all events', iconName: 'precision', isUnlocked: false, progress: 0.0),
|
|
];
|
|
}
|