import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/app_settings.dart'; import '../models/duty_schedule.dart'; import '../models/swap_request.dart'; import 'profile_provider.dart'; import 'supabase_provider.dart'; import 'stream_recovery.dart'; import 'realtime_controller.dart'; final geofenceProvider = FutureProvider((ref) async { final client = ref.watch(supabaseClientProvider); final data = await client .from('app_settings') .select() .eq('key', 'geofence') .maybeSingle(); if (data == null) return null; final setting = AppSetting.fromMap(data); return GeofenceConfig.fromJson(setting.value); }); /// Toggle to show/hide past schedules. Defaults to false (hide past). final showPastSchedulesProvider = StateProvider((ref) => false); final dutySchedulesProvider = StreamProvider>((ref) { final client = ref.watch(supabaseClientProvider); final profileAsync = ref.watch(currentProfileProvider); final profile = profileAsync.valueOrNull; if (profile == null) { return Stream.value(const []); } // All roles now see all schedules (RLS updated in migration) final wrapper = StreamRecoveryWrapper( stream: client .from('duty_schedules') .stream(primaryKey: ['id']) .order('start_time'), onPollData: () async { final data = await client .from('duty_schedules') .select() .order('start_time'); return data.map(DutySchedule.fromMap).toList(); }, fromMap: DutySchedule.fromMap, channelName: 'duty_schedules', onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus, ); ref.onDispose(wrapper.dispose); return wrapper.stream.map((result) => result.data); }); /// Fetch duty schedules by a list of IDs (used by UI when swap requests reference /// schedules that are not included in the current user's `dutySchedulesProvider`). final dutySchedulesByIdsProvider = FutureProvider.family, List>((ref, ids) async { if (ids.isEmpty) return const []; final client = ref.watch(supabaseClientProvider); final quoted = ids.map((id) => '"$id"').join(','); final inList = '($quoted)'; final rows = await client .from('duty_schedules') .select() .filter('id', 'in', inList) as List; return rows .map((r) => DutySchedule.fromMap(r as Map)) .toList(); }); /// Fetch upcoming duty schedules for a specific user (used by swap UI to /// let the requester pick a concrete target shift owned by the recipient). final dutySchedulesForUserProvider = FutureProvider.family, String>((ref, userId) async { final client = ref.watch(supabaseClientProvider); final nowIso = DateTime.now().toUtc().toIso8601String(); final rows = await client .from('duty_schedules') .select() .eq('user_id', userId) /* exclude past schedules by ensuring the shift has not ended */ .gte('end_time', nowIso) .order('start_time') as List; return rows .map((r) => DutySchedule.fromMap(r as Map)) .toList(); }); final swapRequestsProvider = StreamProvider>((ref) { final client = ref.watch(supabaseClientProvider); final profileAsync = ref.watch(currentProfileProvider); final profile = profileAsync.valueOrNull; if (profile == null) { return Stream.value(const []); } final isAdmin = profile.role == 'admin' || profile.role == 'dispatcher'; final wrapper = StreamRecoveryWrapper( stream: isAdmin ? client .from('swap_requests') .stream(primaryKey: ['id']) .order('created_at', ascending: false) : client .from('swap_requests') .stream(primaryKey: ['id']) .order('created_at', ascending: false), onPollData: () async { final data = await client .from('swap_requests') .select() .order('created_at', ascending: false); return data.map(SwapRequest.fromMap).toList(); }, fromMap: SwapRequest.fromMap, channelName: 'swap_requests', onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus, ); ref.onDispose(wrapper.dispose); return wrapper.stream.map((result) { return result.data .where( (row) => row.requesterId == profile.id || row.recipientId == profile.id, ) .toList(); }); }); final workforceControllerProvider = Provider((ref) { final client = ref.watch(supabaseClientProvider); return WorkforceController(client); }); class WorkforceController { WorkforceController(this._client); final SupabaseClient _client; Future generateSchedule({ required DateTime startDate, required DateTime endDate, }) async { await _client.rpc( 'generate_duty_schedule', params: { 'start_date': _formatDate(startDate), 'end_date': _formatDate(endDate), }, ); } Future insertSchedules(List> schedules) async { if (schedules.isEmpty) return; await _client.from('duty_schedules').insert(schedules); } Future checkIn({ required String dutyScheduleId, required double lat, required double lng, }) async { final data = await _client.rpc( 'duty_check_in', params: {'p_duty_id': dutyScheduleId, 'p_lat': lat, 'p_lng': lng}, ); return data as String?; } Future requestSwap({ required String requesterScheduleId, required String targetScheduleId, required String recipientId, }) async { final data = await _client.rpc( 'request_shift_swap', params: { 'p_shift_id': requesterScheduleId, 'p_target_shift_id': targetScheduleId, 'p_recipient_id': recipientId, }, ); return data as String?; } Future respondSwap({ required String swapId, required String action, }) async { await _client.rpc( 'respond_shift_swap', params: {'p_swap_id': swapId, 'p_action': action}, ); } /// Reassign the recipient of a swap request. Only admins/dispatchers are /// expected to call this; the DB RLS and RPCs will additionally enforce rules. Future reassignSwap({ required String swapId, required String newRecipientId, }) async { // Prefer using an RPC for server-side validation, but update directly here await _client .from('swap_requests') .update({ 'recipient_id': newRecipientId, 'status': 'pending', 'updated_at': DateTime.now().toUtc().toIso8601String(), }) .eq('id', swapId); } Future updateSchedule({ required String scheduleId, required String userId, required String shiftType, required DateTime startTime, required DateTime endTime, }) async { await _client .from('duty_schedules') .update({ 'user_id': userId, 'shift_type': shiftType, 'start_time': startTime.toUtc().toIso8601String(), 'end_time': endTime.toUtc().toIso8601String(), }) .eq('id', scheduleId); } String _formatDate(DateTime value) { final date = DateTime(value.year, value.month, value.day); final month = date.month.toString().padLeft(2, '0'); final day = date.day.toString().padLeft(2, '0'); return '${date.year}-$month-$day'; } }