- ApiClient.uploadFile() — multipart POST to /v1/upload/file (60s timeout)
- ApiEndpoints.uploadFile — points to Node.js upload endpoint
- GamificationService.submitContribution() now uploads each picked image
to OneDrive via the server upload pipeline, then passes the returned
{ fileId, url, ... } objects as `media` in the submission body
(replaces broken behaviour of sending local device paths as strings)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
180 lines
6.3 KiB
Dart
180 lines
6.3 KiB
Dart
// 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);
|
|
|
|
/// Get event types (POST to /events/type-list/)
|
|
/// Cached for 30 minutes since event types rarely change.
|
|
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);
|
|
final list = <EventTypeModel>[];
|
|
final data = res['event_types'] ?? res;
|
|
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();
|
|
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,
|
|
);
|
|
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();
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/// Event details — requiresAuth: false so guests can fetch full details
|
|
Future<EventModel> getEventDetails(int eventId) async {
|
|
final res = await _api.post(ApiEndpoints.eventDetails, body: {'event_id': eventId}, requiresAuth: false);
|
|
return EventModel.fromJson(Map<String, dynamic>.from(res));
|
|
}
|
|
|
|
/// 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.
|
|
Future<List<EventModel>> getEventsByLocation(double lat, double lng, {double radiusKm = 10.0}) async {
|
|
final body = {
|
|
'latitude': lat,
|
|
'longitude': lng,
|
|
'radius_km': radiusKm,
|
|
'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)));
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
|
|
/// Featured events for the home screen hero carousel.
|
|
Future<List<EventModel>> getFeaturedEvents() async {
|
|
final res = await _api.post(ApiEndpoints.featuredEvents, requiresAuth: false);
|
|
final events = res['events'] ?? res['data'] ?? [];
|
|
if (events is List) {
|
|
return events
|
|
.whereType<Map<String, dynamic>>()
|
|
.map((e) => EventModel.fromJson(e))
|
|
.toList();
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/// Top events for the home screen top events section.
|
|
Future<List<EventModel>> getTopEvents() async {
|
|
final res = await _api.post(ApiEndpoints.topEvents, requiresAuth: false);
|
|
final events = res['events'] ?? res['data'] ?? [];
|
|
if (events is List) {
|
|
return events
|
|
.whereType<Map<String, dynamic>>()
|
|
.map((e) => EventModel.fromJson(e))
|
|
.toList();
|
|
}
|
|
return [];
|
|
}
|
|
|
|
/// 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);
|
|
return res;
|
|
}
|
|
|
|
/// Convenience: get events for a specific date (YYYY-MM-DD).
|
|
/// Uses the cached events list when available to avoid redundant API calls.
|
|
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)));
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}).toList();
|
|
}
|
|
}
|