Files
Eventify-frontend/lib/features/events/services/events_service.dart

160 lines
5.8 KiB
Dart
Raw Normal View History

2026-01-31 15:23:18 +05:30
// lib/features/events/services/events_service.dart
import '../../../core/api/api_client.dart';
import '../../../core/api/api_endpoints.dart';
import '../models/event_models.dart';
class EventsService {
final ApiClient _api = ApiClient();
// ---------------------------------------------------------------------------
// In-memory caches with TTL
// ---------------------------------------------------------------------------
static List<EventTypeModel>? _cachedTypes;
static DateTime? _typesCacheTime;
static const _typesCacheTTL = Duration(minutes: 30);
static List<EventModel>? _cachedAllEvents;
static DateTime? _eventsCacheTime;
static const _eventsCacheTTL = Duration(minutes: 5);
2026-01-31 15:23:18 +05:30
/// Get event types (POST to /events/type-list/)
/// Cached for 30 minutes since event types rarely change.
2026-01-31 15:23:18 +05:30
Future<List<EventTypeModel>> getEventTypes() async {
if (_cachedTypes != null &&
_typesCacheTime != null &&
DateTime.now().difference(_typesCacheTime!) < _typesCacheTTL) {
return _cachedTypes!;
}
final res = await _api.post(ApiEndpoints.eventTypes, requiresAuth: false);
2026-01-31 15:23:18 +05:30
final list = <EventTypeModel>[];
final data = res['event_types'] ?? res;
2026-01-31 15:23:18 +05:30
if (data is List) {
for (final e in data) {
if (e is Map<String, dynamic>) list.add(EventTypeModel.fromJson(e));
}
}
_cachedTypes = list;
_typesCacheTime = DateTime.now();
2026-01-31 15:23:18 +05:30
return list;
}
/// Get events filtered by pincode with pagination.
/// [page] starts at 1. [pageSize] defaults to 50.
/// Returns a list of events for the requested page.
Future<List<EventModel>> getEventsByPincode(String pincode, {int page = 1, int pageSize = 50, int perType = 5, String q = ''}) async {
// Use cache for 'all' pincode queries (first page only, no active search)
if (pincode == 'all' &&
page == 1 &&
q.isEmpty &&
_cachedAllEvents != null &&
_eventsCacheTime != null &&
DateTime.now().difference(_eventsCacheTime!) < _eventsCacheTTL) {
return _cachedAllEvents!;
}
final Map<String, dynamic> body = {'pincode': pincode, 'page': page, 'page_size': pageSize};
// Diverse mode: fetch a few events per type so all categories are represented
if (perType > 0 && page == 1) body['per_type'] = perType;
// Server-side search filter
if (q.isNotEmpty) body['q'] = q;
final res = await _api.post(
ApiEndpoints.eventsByPincode,
body: body,
requiresAuth: false,
);
2026-01-31 15:23:18 +05:30
final list = <EventModel>[];
final events = res['events'] ?? res['data'] ?? [];
if (events is List) {
for (final e in events) {
if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e)));
}
}
if (pincode == 'all' && page == 1) {
_cachedAllEvents = list;
_eventsCacheTime = DateTime.now();
}
2026-01-31 15:23:18 +05:30
return list;
}
/// Event details — requiresAuth: false so guests can fetch full details
2026-01-31 15:23:18 +05:30
Future<EventModel> getEventDetails(int eventId) async {
final res = await _api.post(ApiEndpoints.eventDetails, body: {'event_id': eventId}, requiresAuth: false);
2026-01-31 15:23:18 +05:30
return EventModel.fromJson(Map<String, dynamic>.from(res));
}
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
/// Related events by event_type_id (EVT-002).
/// Fetches events with the same category, silently returns [] on failure.
Future<List<EventModel>> getEventsByCategory(int eventTypeId, {int limit = 5}) async {
try {
final res = await _api.post(
ApiEndpoints.eventsByCategory,
body: {'event_type_id': eventTypeId, 'page_size': limit, 'page': 1},
requiresAuth: false,
);
final results = res['results'] ?? res['events'] ?? res['data'] ?? [];
if (results is List) {
return results
.whereType<Map<String, dynamic>>()
.map((e) => EventModel.fromJson(e))
.toList();
}
} catch (_) {
// silently fail — related events are non-critical
}
return [];
}
/// Get events by GPS coordinates using haversine distance filtering.
/// Automatically expands radius (10 → 25 → 50 → 100 km) until ≥ 6 events found.
Future<List<EventModel>> getEventsByLocation(double lat, double lng, {double initialRadiusKm = 10}) async {
const radii = [10.0, 25.0, 50.0, 100.0];
for (final radius in radii) {
if (radius < initialRadiusKm) continue;
final body = {
'latitude': lat,
'longitude': lng,
'radius_km': radius,
'page': 1,
'page_size': 50,
'per_type': 5,
};
final res = await _api.post(ApiEndpoints.eventsByPincode, body: body, requiresAuth: false);
final list = <EventModel>[];
final events = res['events'] ?? res['data'] ?? [];
if (events is List) {
for (final e in events) {
if (e is Map<String, dynamic>) list.add(EventModel.fromJson(Map<String, dynamic>.from(e)));
}
}
if (list.length >= 6 || radius >= 100) return list;
}
return [];
}
2026-01-31 15:23:18 +05:30
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
2026-01-31 15:23:18 +05:30
return res;
}
/// Convenience: get events for a specific date (YYYY-MM-DD).
/// Uses the cached events list when available to avoid redundant API calls.
2026-01-31 15:23:18 +05:30
Future<List<EventModel>> getEventsForDate(String date) async {
final all = await getEventsByPincode('all');
return all.where((e) {
try {
return e.startDate == date ||
e.endDate == date ||
(DateTime.parse(e.startDate).isBefore(DateTime.parse(date)) &&
DateTime.parse(e.endDate).isAfter(DateTime.parse(date)));
2026-01-31 15:23:18 +05:30
} catch (_) {
return false;
}
}).toList();
}
}