Files

209 lines
8.6 KiB
Dart
Raw Permalink Normal View History

// 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,
);
}
feat: Phase 3 — 26 medium-priority gaps implemented P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear Notionists avatar via TierAvatarRing, district picker (14 pills) with 183-day cooldown lock, multipart photo upload to server P3-B Home: Top Events converted to PageView scroll-snap (viewportFraction 0.9 + PageScrollPhysics) P3-C Event detail: contributor widget (tier ring + name + navigation), related events horizontal row; added getEventsByCategory() to EventsService; added contributorId/Name/Tier fields to EventModel P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as asset, async-loaded in SearchScreen replacing hardcoded 32 cities P3-E Checkout: promo code field + Apply/Remove button in Step 2, discountAmount subtracted from total, applyPromo()/resetPromo() methods in CheckoutProvider P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→ amber) in contribute + profile screens; TierAvatarRing in podium and all leaderboard rows; GlassCard current-user stats card at top of leaderboard tab P3-H New ContributorProfileScreen: tier ring, stats, submission grid with status chips; getDashboardForUser() in GamificationService; wired from leaderboard row taps P3-I Achievements: 11 default badges (up from 6), 6 new icon map entries; progress % labels already confirmed present P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px) replaces large rating number in ReviewSummary P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles; share button wired in profile header and leaderboard tab P3-M SafeArea audit: home bottom nav, contribute/achievements scroll padding, profile CustomScrollView top inset New files: tier_avatar_ring.dart, glass_card.dart, eventify_bottom_sheet.dart, contributor_profile_screen.dart, share_rank_card.dart, assets/data/kerala_pincodes.json New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
// ---------------------------------------------------------------------------
// 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);
feat: Phase 3 — 26 medium-priority gaps implemented P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear Notionists avatar via TierAvatarRing, district picker (14 pills) with 183-day cooldown lock, multipart photo upload to server P3-B Home: Top Events converted to PageView scroll-snap (viewportFraction 0.9 + PageScrollPhysics) P3-C Event detail: contributor widget (tier ring + name + navigation), related events horizontal row; added getEventsByCategory() to EventsService; added contributorId/Name/Tier fields to EventModel P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as asset, async-loaded in SearchScreen replacing hardcoded 32 cities P3-E Checkout: promo code field + Apply/Remove button in Step 2, discountAmount subtracted from total, applyPromo()/resetPromo() methods in CheckoutProvider P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→ amber) in contribute + profile screens; TierAvatarRing in podium and all leaderboard rows; GlassCard current-user stats card at top of leaderboard tab P3-H New ContributorProfileScreen: tier ring, stats, submission grid with status chips; getDashboardForUser() in GamificationService; wired from leaderboard row taps P3-I Achievements: 11 default badges (up from 6), 6 new icon map entries; progress % labels already confirmed present P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px) replaces large rating number in ReviewSummary P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles; share button wired in profile header and leaderboard tab P3-M SafeArea audit: home bottom nav, contribute/achievements scroll padding, profile CustomScrollView top inset New files: tier_avatar_ring.dart, glass_card.dart, eventify_bottom_sheet.dart, contributor_profile_screen.dart, share_rank_card.dart, assets/data/kerala_pincodes.json New dep: path_provider ^2.1.0
2026-04-04 17:17:36 +05:30
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),
];
}