Phase 2 gaps completed: - HOME-001: Hero slider pause-on-touch (GestureDetector wraps PageView) - HOME-003: Calendar bottom sheet with TableCalendar (replaces custom dialog) - AUTH-004: District dropdown on signup (14 Kerala districts) - EVT-003: Mobile sticky "Book Now" bar + desktop CTA wired to CheckoutScreen - ACH-001: Real achievements from dashboard API with fallback defaults - GAM-002: 3-card EP row (Lifetime EP / Liquid EP / Reward Points) - GAM-005: Horizontal tier roadmap Bronze→Silver→Gold→Platinum→Diamond - CTR-001: Submission status chips (PENDING/APPROVED/REJECTED) - CTR-002: +EP badge on approved submissions - PROF-003: Gamification cards on profile screen with Consumer<GamificationProvider> - UX-001: Shimmer skeleton loaders (shimmer ^3.0.0) replacing CircularProgressIndicator Already complete (verified, no changes needed): - HOME-002: Category shelves already built in _buildTypeSection() - LDR-002: Podium visualization already built (_buildPodium / _buildDesktopPodium) - BOOK-004: UPI handled natively by Razorpay SDK Deferred: LOC-001/002 (needs Django haversine endpoint) Skipped: AUTH-002 (OTP needs SMS provider decision) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
163 lines
6.6 KiB
Dart
163 lines
6.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=$email';
|
|
final res = await _api.post(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.post(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.post(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
|
|
// POST /v1/gamification/submit-event body: event data
|
|
// ---------------------------------------------------------------------------
|
|
Future<void> submitContribution(Map<String, dynamic> data) async {
|
|
final email = await _getUserEmail();
|
|
final body = <String, dynamic>{'user_id': email, ...data};
|
|
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: 'First Submission', description: 'Submitted your first event.', iconName: 'edit', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-02', title: 'Silver Streak', description: 'Reached Silver tier.', iconName: 'star', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-03', title: 'Gold Rush', description: 'Reach Gold tier (500 EP).', iconName: 'emoji_events', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-04', title: 'Top 10', description: 'Appear in the district leaderboard top 10.', iconName: 'leaderboard', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-05', title: 'Image Pro', description: 'Submit 10 events with 3+ images.', iconName: 'photo_library', isUnlocked: false, progress: 0.0),
|
|
AchievementBadge(id: 'badge-06', title: 'Pioneer', description: 'One of the first 100 contributors.', iconName: 'verified', isUnlocked: false, progress: 0.0),
|
|
];
|
|
}
|