// lib/features/gamification/models/gamification_models.dart // Data models matching TechDocs v2 DB schema for the Contributor Module. // --------------------------------------------------------------------------- // Tier enum — matches PRD v3 §4.4 thresholds (based on Lifetime EP) // --------------------------------------------------------------------------- enum ContributorTier { BRONZE, SILVER, GOLD, PLATINUM, DIAMOND } /// Returns the correct tier for a given lifetime EP total. ContributorTier tierFromEp(int lifetimeEp) { if (lifetimeEp >= 5000) return ContributorTier.DIAMOND; if (lifetimeEp >= 1500) return ContributorTier.PLATINUM; if (lifetimeEp >= 500) return ContributorTier.GOLD; if (lifetimeEp >= 100) return ContributorTier.SILVER; return ContributorTier.BRONZE; } /// Human-readable label for a tier. String tierLabel(ContributorTier tier) { switch (tier) { case ContributorTier.BRONZE: return 'Bronze'; case ContributorTier.SILVER: return 'Silver'; case ContributorTier.GOLD: return 'Gold'; case ContributorTier.PLATINUM: return 'Platinum'; case ContributorTier.DIAMOND: return 'Diamond'; } } /// EP threshold for next tier (used for progress bar). Returns null at max tier. int? nextTierThreshold(ContributorTier tier) { switch (tier) { case ContributorTier.BRONZE: return 100; case ContributorTier.SILVER: return 500; case ContributorTier.GOLD: return 1500; case ContributorTier.PLATINUM: return 5000; case ContributorTier.DIAMOND: return null; } } /// Lower EP bound for current tier (used for progress bar calculation). int tierStartEp(ContributorTier tier) { switch (tier) { case ContributorTier.BRONZE: return 0; case ContributorTier.SILVER: return 100; case ContributorTier.GOLD: return 500; case ContributorTier.PLATINUM: return 1500; case ContributorTier.DIAMOND: return 5000; } } // --------------------------------------------------------------------------- // UserGamificationProfile — mirrors the `UserGamificationProfile` DB table // --------------------------------------------------------------------------- class UserGamificationProfile { final String userId; 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; const UserGamificationProfile({ required this.userId, required this.lifetimeEp, required this.currentEp, required this.currentRp, required this.tier, }); factory UserGamificationProfile.fromJson(Map json) { final ep = (json['lifetime_ep'] as int?) ?? 0; return UserGamificationProfile( userId: json['user_id'] as String? ?? '', lifetimeEp: ep, currentEp: (json['current_ep'] as int?) ?? 0, currentRp: (json['current_rp'] as int?) ?? 0, tier: tierFromEp(ep), ); } } // --------------------------------------------------------------------------- // LeaderboardEntry — maps from Node.js API response fields // --------------------------------------------------------------------------- class LeaderboardEntry { final int rank; final String username; final String? avatarUrl; final int lifetimeEp; final int monthlyPoints; final ContributorTier tier; final int eventsCount; final bool isCurrentUser; final String? district; const LeaderboardEntry({ required this.rank, required this.username, this.avatarUrl, required this.lifetimeEp, this.monthlyPoints = 0, required this.tier, required this.eventsCount, this.isCurrentUser = false, this.district, }); factory LeaderboardEntry.fromJson(Map json) { // Node.js API returns 'points' for lifetime EP and 'name' for username final ep = (json['points'] as num?)?.toInt() ?? (json['lifetime_ep'] as num?)?.toInt() ?? 0; final tierStr = json['level'] as String? ?? json['tier'] as String?; return LeaderboardEntry( rank: (json['rank'] as num?)?.toInt() ?? 0, username: json['name'] as String? ?? json['username'] as String? ?? '', avatarUrl: json['avatar_url'] as String?, lifetimeEp: ep, monthlyPoints: (json['monthlyPoints'] as num?)?.toInt() ?? 0, tier: tierStr != null ? _tierFromString(tierStr) : tierFromEp(ep), eventsCount: (json['eventsAdded'] as num?)?.toInt() ?? (json['events_count'] as num?)?.toInt() ?? 0, isCurrentUser: (json['is_current_user'] as bool?) ?? false, district: json['district'] as String?, ); } } /// Parse tier string from API (e.g. "Gold") to enum. ContributorTier _tierFromString(String s) { switch (s.toLowerCase()) { case 'diamond': return ContributorTier.DIAMOND; case 'platinum': return ContributorTier.PLATINUM; case 'gold': return ContributorTier.GOLD; case 'silver': return ContributorTier.SILVER; default: return ContributorTier.BRONZE; } } // --------------------------------------------------------------------------- // CurrentUserStats — from leaderboard API's currentUser field // --------------------------------------------------------------------------- class CurrentUserStats { final int rank; final int points; final int monthlyPoints; final String level; final int rewardCycleDays; final int eventsAdded; final String? district; const CurrentUserStats({ required this.rank, required this.points, this.monthlyPoints = 0, required this.level, this.rewardCycleDays = 0, this.eventsAdded = 0, this.district, }); factory CurrentUserStats.fromJson(Map json) { return CurrentUserStats( rank: (json['rank'] as num?)?.toInt() ?? 0, points: (json['points'] as num?)?.toInt() ?? 0, monthlyPoints: (json['monthlyPoints'] as num?)?.toInt() ?? 0, level: json['level'] as String? ?? 'Bronze', rewardCycleDays: (json['rewardCycleDays'] as num?)?.toInt() ?? 0, eventsAdded: (json['eventsAdded'] as num?)?.toInt() ?? 0, district: json['district'] as String?, ); } } // --------------------------------------------------------------------------- // LeaderboardResponse — wraps the full leaderboard API response // --------------------------------------------------------------------------- class LeaderboardResponse { final List entries; final CurrentUserStats? currentUser; final int totalParticipants; const LeaderboardResponse({ required this.entries, this.currentUser, this.totalParticipants = 0, }); } // --------------------------------------------------------------------------- // SubmissionModel — event submissions from dashboard API // --------------------------------------------------------------------------- class SubmissionModel { final String id; final String eventName; final String category; final String status; // PENDING, APPROVED, REJECTED final String? district; final int epAwarded; final DateTime createdAt; final List images; const SubmissionModel({ required this.id, required this.eventName, this.category = '', required this.status, this.district, this.epAwarded = 0, required this.createdAt, this.images = const [], }); factory SubmissionModel.fromJson(Map json) { final rawImages = json['images'] as List? ?? []; return SubmissionModel( id: (json['id'] ?? json['submission_id'] ?? '').toString(), eventName: json['event_name'] as String? ?? '', category: json['category'] as String? ?? '', status: json['status'] as String? ?? 'PENDING', district: json['district'] as String?, epAwarded: (json['total_ep_awarded'] as num?)?.toInt() ?? (json['ep_awarded'] as num?)?.toInt() ?? 0, createdAt: DateTime.tryParse(json['created_at'] as String? ?? '') ?? DateTime.now(), images: rawImages.map((e) => e.toString()).toList(), ); } } // --------------------------------------------------------------------------- // DashboardResponse — wraps the full dashboard API response // --------------------------------------------------------------------------- class DashboardResponse { final UserGamificationProfile profile; final List submissions; const DashboardResponse({ required this.profile, this.submissions = const [], }); } // --------------------------------------------------------------------------- // ShopItem — mirrors `RedeemShopItem` table // --------------------------------------------------------------------------- class ShopItem { final String id; final String name; final String description; final int rpCost; final int stockQuantity; final String? imageUrl; const ShopItem({ required this.id, required this.name, required this.description, required this.rpCost, required this.stockQuantity, this.imageUrl, }); factory ShopItem.fromJson(Map json) { return ShopItem( id: json['id'] as String? ?? '', name: json['name'] as String? ?? '', description: json['description'] as String? ?? '', rpCost: (json['rp_cost'] as int?) ?? 0, stockQuantity: (json['stock_quantity'] as int?) ?? 0, imageUrl: json['image_url'] as String?, ); } } // --------------------------------------------------------------------------- // RedemptionRecord — mirrors `RedemptionHistory` table // --------------------------------------------------------------------------- class RedemptionRecord { final String id; final String itemId; final int rpSpent; final String voucherCode; final DateTime timestamp; const RedemptionRecord({ required this.id, required this.itemId, required this.rpSpent, required this.voucherCode, required this.timestamp, }); factory RedemptionRecord.fromJson(Map json) { return RedemptionRecord( id: json['id'] as String? ?? '', itemId: json['item_id'] as String? ?? '', rpSpent: (json['rp_spent'] as int?) ?? 0, voucherCode: json['voucher_code_issued'] as String? ?? '', timestamp: DateTime.tryParse(json['timestamp'] as String? ?? '') ?? DateTime.now(), ); } } // --------------------------------------------------------------------------- // AchievementBadge // --------------------------------------------------------------------------- class AchievementBadge { final String id; final String title; final String description; final String iconName; // maps to an IconData key final bool isUnlocked; final double progress; // 0.0 – 1.0 const AchievementBadge({ required this.id, required this.title, required this.description, required this.iconName, required this.isUnlocked, required this.progress, }); }