release: bump version to 1.4(p) (versionCode 14)
- Update versionCode 12 → 14, versionName 1.3(p) → 1.4(p) - Update pubspec.yaml version to 1.4.0+14 - Add CHANGELOG.md with full version history - Update README.md: version badge + changelog section - Desktop Contribute Dashboard rebuilt to match web version - Contributor Dashboard title, 3-tab nav (Contribute/Leaderboard/Achievements) - Two-column submit form, tier milestone progress bar - Desktop leaderboard with podium, filters, rank table (green points) - Desktop achievements 3-column badge grid - Inline Reward Shop with RP balance - Gamification feature module (EP, RP, leaderboard, achievements, shop) - Profile screen redesigned to match web app layout with animations - Home screen bottom sheet date filter chips - Updated API endpoints, login/event detail screens, theme colors - Added Gilroy font suite, responsive layout improvements
This commit is contained in:
212
lib/features/gamification/models/gamification_models.dart
Normal file
212
lib/features/gamification/models/gamification_models.dart
Normal file
@@ -0,0 +1,212 @@
|
||||
// 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<String, dynamic> 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
|
||||
// ---------------------------------------------------------------------------
|
||||
class LeaderboardEntry {
|
||||
final int rank;
|
||||
final String username;
|
||||
final String? avatarUrl;
|
||||
final int lifetimeEp;
|
||||
final ContributorTier tier;
|
||||
final int eventsCount;
|
||||
final bool isCurrentUser;
|
||||
|
||||
const LeaderboardEntry({
|
||||
required this.rank,
|
||||
required this.username,
|
||||
this.avatarUrl,
|
||||
required this.lifetimeEp,
|
||||
required this.tier,
|
||||
required this.eventsCount,
|
||||
this.isCurrentUser = false,
|
||||
});
|
||||
|
||||
factory LeaderboardEntry.fromJson(Map<String, dynamic> json) {
|
||||
final ep = (json['lifetime_ep'] as int?) ?? 0;
|
||||
return LeaderboardEntry(
|
||||
rank: (json['rank'] as int?) ?? 0,
|
||||
username: json['username'] as String? ?? '',
|
||||
avatarUrl: json['avatar_url'] as String?,
|
||||
lifetimeEp: ep,
|
||||
tier: tierFromEp(ep),
|
||||
eventsCount: (json['events_count'] as int?) ?? 0,
|
||||
isCurrentUser: (json['is_current_user'] as bool?) ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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<String, dynamic> 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<String, dynamic> 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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user