feat: Phase 1 critical gaps — gamification API, Razorpay checkout, Google OAuth, notifications

- Fix gamification endpoints to use Node.js server (app.eventifyplus.com)
- Replace 6 mock gamification methods with real API calls (dashboard, leaderboard, shop, redeem, submit)
- Add booking models, service, payment service (Razorpay), checkout provider
- Add 3-step CheckoutScreen with Razorpay native modal integration
- Add Google OAuth login (Flutter + Django backend)
- Add full notifications system (Django model + 3 endpoints + Flutter UI)
- Register CheckoutProvider, NotificationProvider in main.dart MultiProvider
- Wire notification bell in HomeScreen app bar
- Add razorpay_flutter ^1.3.7 and google_sign_in ^6.2.2 packages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 15:46:53 +05:30
parent 847577c09d
commit 29e326b8fc
24 changed files with 1663 additions and 164 deletions

View File

@@ -94,41 +94,162 @@ class UserGamificationProfile {
}
// ---------------------------------------------------------------------------
// LeaderboardEntry
// 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<String, dynamic> json) {
final ep = (json['lifetime_ep'] as int?) ?? 0;
// 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 int?) ?? 0,
username: json['username'] as String? ?? '',
rank: (json['rank'] as num?)?.toInt() ?? 0,
username: json['name'] as String? ?? json['username'] as String? ?? '',
avatarUrl: json['avatar_url'] as String?,
lifetimeEp: ep,
tier: tierFromEp(ep),
eventsCount: (json['events_count'] as int?) ?? 0,
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<String, dynamic> 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<LeaderboardEntry> 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<String> 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<String, dynamic> 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<SubmissionModel> submissions;
const DashboardResponse({
required this.profile,
this.submissions = const [],
});
}
// ---------------------------------------------------------------------------
// ShopItem — mirrors `RedeemShopItem` table
// ---------------------------------------------------------------------------