import 'package:flutter/foundation.dart' show debugPrint; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../brick/cache_warmer.dart'; import '../models/user_office.dart'; import '../utils/snackbar.dart' show isOfflineSaveError; import 'connectivity_provider.dart'; import 'supabase_provider.dart'; import 'stream_recovery.dart'; import 'realtime_controller.dart'; // --------------------------------------------------------------------------- // Offline-pending state // --------------------------------------------------------------------------- /// Queued assign/remove operations made offline. /// Each entry: {'op': 'assign'|'remove', 'user_id': ..., 'office_id': ...} final offlinePendingUserOfficeOpsProvider = StateProvider>>((ref) => const []); // --------------------------------------------------------------------------- // Read provider // --------------------------------------------------------------------------- final userOfficesProvider = StreamProvider>((ref) { final client = ref.watch(supabaseClientProvider); final pendingOps = ref.watch(offlinePendingUserOfficeOpsProvider); List applyPending(List rows) { if (pendingOps.isEmpty) return rows; var result = List.from(rows); for (final op in pendingOps) { final userId = op['user_id'] as String; final officeId = op['office_id'] as String; if (op['op'] == 'assign') { final exists = result.any( (r) => r.userId == userId && r.officeId == officeId, ); if (!exists) result.add(UserOffice(userId: userId, officeId: officeId)); } else if (op['op'] == 'remove') { result.removeWhere( (r) => r.userId == userId && r.officeId == officeId, ); } } return result; } final wrapper = StreamRecoveryWrapper( stream: client .from('user_offices') .stream(primaryKey: ['user_id', 'office_id']) .order('created_at'), onPollData: () async { final data = await client.from('user_offices').select().order('created_at'); return data.map(UserOffice.fromMap).toList(); }, fromMap: UserOffice.fromMap, channelName: 'user_offices', onStatusChanged: ref.read(realtimeControllerProvider).handleChannelStatus, onOfflineData: () async { final rows = await CacheWarmer.readCachedUserOffices(); return applyPending(rows.map(UserOffice.fromMap).toList()); }, ); ref.onDispose(wrapper.dispose); // Reconnect-replay ref.listen(isOnlineProvider, (wasOnline, isNowOnline) async { if (wasOnline == true || !isNowOnline) return; final pending = List>.from( ref.read(offlinePendingUserOfficeOpsProvider), ); if (pending.isEmpty) return; debugPrint( '[userOfficesProvider] reconnected — syncing ${pending.length} office op(s)', ); final synced = []; for (var i = 0; i < pending.length; i++) { final op = pending[i]; final userId = op['user_id'] as String; final officeId = op['office_id'] as String; try { if (op['op'] == 'assign') { await client.from('user_offices').insert({ 'user_id': userId, 'office_id': officeId, }); } else { await client .from('user_offices') .delete() .eq('user_id', userId) .eq('office_id', officeId); } synced.add(i); } catch (e) { final msg = e.toString(); if (msg.contains('23505') || msg.contains('duplicate key')) { synced.add(i); } else { debugPrint( '[userOfficesProvider] failed to sync op $i: $e', ); } } } if (synced.isNotEmpty) { final remaining = >[]; for (var i = 0; i < pending.length; i++) { if (!synced.contains(i)) remaining.add(pending[i]); } ref.read(offlinePendingUserOfficeOpsProvider.notifier).state = List.unmodifiable(remaining); } }); return wrapper.stream.map((result) => applyPending(result.data)); }); // --------------------------------------------------------------------------- // Controller // --------------------------------------------------------------------------- final userOfficesControllerProvider = Provider((ref) { final client = ref.watch(supabaseClientProvider); return UserOfficesController(client, ref); }); class UserOfficesController { UserOfficesController(this._client, [this._ref]); final SupabaseClient _client; final Ref? _ref; /// Assign a user to an office. Offline: queues the operation. Future assignUserOffice({ required String userId, required String officeId, }) async { try { await _client.from('user_offices').insert({ 'user_id': userId, 'office_id': officeId, }); } catch (e) { if (!isOfflineSaveError(e)) rethrow; _queueOp('assign', userId, officeId); } } /// Remove a user from an office. Offline: queues the operation. Future removeUserOffice({ required String userId, required String officeId, }) async { try { await _client .from('user_offices') .delete() .eq('user_id', userId) .eq('office_id', officeId); } catch (e) { if (!isOfflineSaveError(e)) rethrow; _queueOp('remove', userId, officeId); } } void _queueOp(String op, String userId, String officeId) { final ref = _ref; if (ref == null) return; final current = List>.from( ref.read(offlinePendingUserOfficeOpsProvider), ); // Dedupe: collapse redundant assign/remove pairs for the same user+office current.removeWhere( (e) => e['user_id'] == userId && e['office_id'] == officeId, ); current.add({'op': op, 'user_id': userId, 'office_id': officeId}); ref.read(offlinePendingUserOfficeOpsProvider.notifier).state = List.unmodifiable(current); } }