From 07c53818a59ffc69b48c87ae09c3195019a1a52d Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Sat, 17 May 2025 09:02:06 +0700 Subject: [PATCH] feat: Implement location logs repository with CRUD operations - Added LocationLogsRepository for managing user location logs. - Implemented methods to get user location logs, add a new log, get recent logs, and fetch logs within a time range. feat: Create locations repository for district-based location management - Added LocationsRepository to handle location data by district ID. - Implemented methods to fetch locations by district, get location by ID, find nearby locations, and add new locations. feat: Enhance permissions repository for role-based access control - Added PermissionsRepository to manage permissions. - Implemented methods to fetch all permissions, get permissions by role/resource, check user permissions, create and delete permissions. feat: Update profile repository for user profile management - Enhanced ProfileRepository to manage user profiles. - Implemented methods to upload avatars, update profile information, create profiles if they don't exist, and fetch profiles by user ID/NIK. feat: Develop resources repository for resource management - Added ResourcesRepository to manage application resources. - Implemented methods to fetch all resources, get resources by ID/type, create, update, and delete resources. feat: Create roles repository for role management - Added RolesRepository to manage user roles. - Implemented methods to fetch all roles, get roles by ID/name, create and update roles, and fetch permissions for a role. feat: Enhance users repository for user management - Updated UsersRepository to manage user data and metadata. - Implemented methods to check user roles, update user metadata, email, phone, and password, and search users by name/username/email. - Added functionality to check if a user is banned. --- .../repositories/map/cities_repository.dart | 86 ++++++ .../map/demographics_repository.dart | 159 ++++++++++ .../map/districts_repository.dart | 103 +++++++ .../map/geogrpaphics_repository.dart | 117 ++++++++ .../map/location_logs_repository.dart | 102 +++++++ .../map/locations_repository.dart | 108 +++++++ .../permissions_repository.dart | 169 +++++++++++ .../personalization/profile_repository.dart | 131 +++++++- .../personalization/resources_repository.dart | 128 ++++++++ .../personalization/roles_repository.dart | 98 +++++- .../personalization/users_repository.dart | 281 ++++++++++++++---- 11 files changed, 1396 insertions(+), 86 deletions(-) diff --git a/sigap-mobile/lib/src/cores/repositories/map/cities_repository.dart b/sigap-mobile/lib/src/cores/repositories/map/cities_repository.dart index e69de29..2100ad7 100644 --- a/sigap-mobile/lib/src/cores/repositories/map/cities_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/map/cities_repository.dart @@ -0,0 +1,86 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/map/models/cities_model.dart'; + +class CitiesRepository extends GetxController { + static CitiesRepository get instance => Get.find(); + + final _supabase = SupabaseService.instance.client; + final _log = Get.find(); + + // Cache cities to reduce database calls + final RxList _citiesCache = [].obs; + + // Get all cities + Future> getAllCities() async { + try { + // If cache exists, return it + if (_citiesCache.isNotEmpty) { + return _citiesCache; + } + + final response = await _supabase + .from('cities') + .select('*, districts(*), units(*)') + .order('name'); + + final data = response.data as List; + final cities = data.map((json) => CityModel.fromJson(json)).toList(); + + // Update cache + _citiesCache.value = cities; + return cities; + } catch (e) { + _log.e('Error fetching cities: $e'); + throw Exception('Failed to load cities: $e'); + } + } + + // Get city by ID + Future getCityById(String cityId) async { + try { + // Check cache first + final cachedCity = _citiesCache.firstWhereOrNull( + (city) => city.id == cityId, + ); + if (cachedCity != null) { + return cachedCity; + } + + final response = + await _supabase + .from('cities') + .select('*, districts(*), units(*)') + .eq('id', cityId) + .single(); + + return CityModel.fromJson(response.data); + } catch (e) { + _log.e('Error fetching city $cityId: $e'); + throw Exception('Failed to load city details: $e'); + } + } + + // Get default city (Jember) + Future getDefaultCity() async { + try { + final response = + await _supabase + .from('cities') + .select('*, districts(*), units(*)') + .eq('name', 'Jember') + .single(); + + return CityModel.fromJson(response.data); + } catch (e) { + _log.e('Error fetching default city: $e'); + throw Exception('Failed to load default city: $e'); + } + } + + // Clear cache + void clearCache() { + _citiesCache.clear(); + } +} diff --git a/sigap-mobile/lib/src/cores/repositories/map/demographics_repository.dart b/sigap-mobile/lib/src/cores/repositories/map/demographics_repository.dart index e69de29..0562b05 100644 --- a/sigap-mobile/lib/src/cores/repositories/map/demographics_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/map/demographics_repository.dart @@ -0,0 +1,159 @@ +import 'package:get/get.dart'; +import 'package:logger/Logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/map/models/demographics_model.dart'; + +class DemographicsRepository extends GetxController { + static DemographicsRepository get instance => Get.find(); + + final _supabase = SupabaseService.instance.client; + final _log = Get.find(); + + // Get demographic data by district ID + Future> getDemographicsByDistrictId( + String districtId, + ) async { + try { + final response = await _supabase + .from('demographics') + .select('*, districts(*)') + .eq('district_id', districtId) + .order('year', ascending: false); + + final data = response as List; + return data.map((json) => DemographicModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching demographic data for district $districtId: $e'); + throw Exception('Failed to load demographic data: $e'); + } + } + + // Get demographic data for specific year + Future> getDemographicsByYear(int year) async { + try { + final response = await _supabase + .from('demographics') + .select('*, districts(*)') + .eq('year', year); + + final data = response.data as List; + return data.map((json) => DemographicModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching demographic data for year $year: $e'); + throw Exception('Failed to load demographic data: $e'); + } + } + + // Get demographic data by district ID and year + Future getDemographicByDistrictAndYear( + String districtId, + int year, + ) async { + try { + final response = + await _supabase + .from('demographics') + .select('*, districts(*)') + .eq('district_id', districtId) + .eq('year', year) + .maybeSingle(); + + if (response == null) return null; + return DemographicModel.fromJson(response); + } catch (e) { + _log.e( + 'Error fetching demographic data for district $districtId and year $year: $e', + ); + throw Exception('Failed to load demographic data: $e'); + } + } + + // Get years with available demographic data + Future> getAvailableYears() async { + try { + final response = + await _supabase.from('demographics').select('year'); + + final data = response as List; + return data.map((json) => json['year'] as int).toList() + ..sort((a, b) => b.compareTo(a)); // Sort descending + } catch (e) { + _log.e('Error fetching available demographic years: $e'); + throw Exception('Failed to load available years: $e'); + } + } + + // Add or update demographic data + Future upsertDemographic({ + String? id, + required String districtId, + required int population, + required int numberOfUnemployed, + required double populationDensity, + required int year, + }) async { + try { + final data = { + if (id != null) 'id': id, + 'district_id': districtId, + 'population': population, + 'number_of_unemployed': numberOfUnemployed, + 'population_density': populationDensity, + 'year': year, + }; + + final response = + await _supabase + .from('demographics') + .upsert(data) + .select('*, districts(*)') + .single(); + + return DemographicModel.fromJson(response.data); + } catch (e) { + _log.e('Error saving demographic data: $e'); + throw Exception('Failed to save demographic data: $e'); + } + } + + // Get districts with highest population density + Future> getHighestPopulationDensityDistricts( + int year, { + int limit = 10, + }) async { + try { + final response = await _supabase + .from('demographics') + .select('*, districts(*)') + .eq('year', year) + .order('population_density', ascending: false) + .limit(limit); + + final data = response.data as List; + return data.map((json) => DemographicModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching highest population density districts: $e'); + throw Exception('Failed to load population density data: $e'); + } + } + + // Get districts with highest unemployment rate + Future> getHighestUnemploymentDistricts( + int year, { + int limit = 10, + }) async { + try { + // Calculate unemployment rate as number_of_unemployed/population + final response = await _supabase.rpc( + 'get_unemployment_rates', + params: {'selected_year': year, 'limit_count': limit}, + ); + + final data = response.data as List; + return data.map((json) => DemographicModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching highest unemployment districts: $e'); + throw Exception('Failed to load unemployment data: $e'); + } + } +} diff --git a/sigap-mobile/lib/src/cores/repositories/map/districts_repository.dart b/sigap-mobile/lib/src/cores/repositories/map/districts_repository.dart index e69de29..991c9f4 100644 --- a/sigap-mobile/lib/src/cores/repositories/map/districts_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/map/districts_repository.dart @@ -0,0 +1,103 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/map/models/districts_model.dart'; + +class DistrictsRepository extends GetxController { + static DistrictsRepository get instance => Get.find(); + + final _supabase = SupabaseService.instance.client; + final _log = Get.find(); + + // Cache districts to reduce database calls + final RxMap> _districtsByCity = + >{}.obs; + + // Get all districts for a city + Future> getDistrictsByCityId(String cityId) async { + try { + // If cache exists, return it + if (_districtsByCity.containsKey(cityId)) { + return _districtsByCity[cityId]!; + } + + final response = await _supabase + .from('districts') + .select('*, cities(*), units(*)') + .eq('city_id', cityId) + .order('name'); + + final data = response.data as List; + final districts = + data.map((json) => DistrictModel.fromJson(json)).toList(); + + // Update cache + _districtsByCity[cityId] = districts; + return districts; + } catch (e) { + _log.e('Error fetching districts for city $cityId: $e'); + throw Exception('Failed to load districts: $e'); + } + } + + // Get district by ID + Future getDistrictById(String districtId) async { + try { + // Check cache first + for (final cityDistricts in _districtsByCity.values) { + final cachedDistrict = cityDistricts.firstWhereOrNull( + (d) => d.id == districtId, + ); + if (cachedDistrict != null) { + return cachedDistrict; + } + } + + final response = + await _supabase + .from('districts') + .select( + '*, cities(*), units(*), crimes(*), demographics(*), geographics(*), locations(*)', + ) + .eq('id', districtId) + .single(); + + return DistrictModel.fromJson(response.data); + } catch (e) { + _log.e('Error fetching district $districtId: $e'); + throw Exception('Failed to load district details: $e'); + } + } + + // Get districts with crime data + Future> getDistrictsWithCrimeData( + int year, + int? month, + ) async { + try { + String query = 'districts.*, cities(*), crimes!inner(*)'; + + var request = _supabase + .from('districts') + .select(query) + .eq('crimes.year', year); + + if (month != null) { + request = request.eq('crimes.month', month); + } + + final response = await request; + final data = response.data as List; + + return data.map((json) => DistrictModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching districts with crime data: $e'); + throw Exception('Failed to load districts with crime data: $e'); + } + } + + // Clear cache + void clearCache() { + _districtsByCity.clear(); + } +} diff --git a/sigap-mobile/lib/src/cores/repositories/map/geogrpaphics_repository.dart b/sigap-mobile/lib/src/cores/repositories/map/geogrpaphics_repository.dart index e69de29..db11e36 100644 --- a/sigap-mobile/lib/src/cores/repositories/map/geogrpaphics_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/map/geogrpaphics_repository.dart @@ -0,0 +1,117 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/map/models/geographics_model.dart'; + +class GeographicsRepository extends GetxController { + static GeographicsRepository get instance => Get.find(); + + final _supabase = SupabaseService.instance.client; + final _log = Get.find(); + + // Get geographic data by district ID + Future> getGeographicsByDistrictId( + String districtId, + ) async { + try { + final response = await _supabase + .from('geographics') + .select('*, districts(*)') + .eq('district_id', districtId); + + final data = response.data as List; + return data.map((json) => GeographicModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching geographic data for district $districtId: $e'); + throw Exception('Failed to load geographic data: $e'); + } + } + + // Get geographic data by ID + Future getGeographicById(String geographicId) async { + try { + final response = + await _supabase + .from('geographics') + .select('*, districts(*)') + .eq('id', geographicId) + .single(); + + return GeographicModel.fromJson(response.data); + } catch (e) { + _log.e('Error fetching geographic data $geographicId: $e'); + throw Exception('Failed to load geographic details: $e'); + } + } + + // Get geographic data by type + Future> getGeographicsByType(String type) async { + try { + final response = await _supabase + .from('geographics') + .select('*, districts(*)') + .eq('type', type); + + final data = response.data as List; + return data.map((json) => GeographicModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching geographic data of type $type: $e'); + throw Exception('Failed to load geographic data: $e'); + } + } + + // Get geographic data for a specific year + Future> getGeographicsForYear(int year) async { + try { + final response = await _supabase + .from('geographics') + .select('*, districts(*)') + .eq('year', year); + + final data = response.data as List; + return data.map((json) => GeographicModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching geographic data for year $year: $e'); + throw Exception('Failed to load geographic data: $e'); + } + } + + // Add or update geographic data + Future upsertGeographic({ + String? id, + required String districtId, + required double latitude, + required double longitude, + String? description, + String? type, + String? address, + double? landArea, + int? year, + }) async { + try { + final data = { + if (id != null) 'id': id, + 'district_id': districtId, + 'latitude': latitude, + 'longitude': longitude, + 'description': description, + 'type': type, + 'address': address, + 'land_area': landArea, + 'year': year, + }; + + final response = + await _supabase + .from('geographics') + .upsert(data) + .select('*, districts(*)') + .single(); + + return GeographicModel.fromJson(response.data); + } catch (e) { + _log.e('Error saving geographic data: $e'); + throw Exception('Failed to save geographic data: $e'); + } + } +} diff --git a/sigap-mobile/lib/src/cores/repositories/map/location_logs_repository.dart b/sigap-mobile/lib/src/cores/repositories/map/location_logs_repository.dart index e69de29..1a63292 100644 --- a/sigap-mobile/lib/src/cores/repositories/map/location_logs_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/map/location_logs_repository.dart @@ -0,0 +1,102 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/map/models/location_logs_model.dart'; + +class LocationLogsRepository extends GetxController { + static LocationLogsRepository get instance => Get.find(); + + final _supabase = SupabaseService.instance.client; + final _log = Get.find(); + + // Get location logs for a specific user + Future> getUserLocationLogs( + String userId, { + int limit = 50, + }) async { + try { + final response = await _supabase + .from('location_logs') + .select('*, users(*)') + .eq('user_id', userId) + .order('timestamp', ascending: false) + .limit(limit); + + final data = response.data as List; + return data.map((json) => LocationLogModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching location logs for user $userId: $e'); + throw Exception('Failed to load location logs: $e'); + } + } + + // Add a new location log + Future addLocationLog({ + required String userId, + required double latitude, + required double longitude, + String? description, + }) async { + try { + final data = { + 'user_id': userId, + 'latitude': latitude, + 'longitude': longitude, + 'timestamp': DateTime.now().toIso8601String(), + 'description': description, + }; + + final response = + await _supabase + .from('location_logs') + .insert(data) + .select('*, users(*)') + .single(); + + return LocationLogModel.fromJson(response.data); + } catch (e) { + _log.e('Error adding location log: $e'); + throw Exception('Failed to add location log: $e'); + } + } + + // Get recent location logs for all users + Future> getRecentLocationLogs({ + int limit = 100, + }) async { + try { + final response = await _supabase + .from('location_logs') + .select('*, users(*)') + .order('timestamp', ascending: false) + .limit(limit); + + final data = response.data as List; + return data.map((json) => LocationLogModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching recent location logs: $e'); + throw Exception('Failed to load recent location logs: $e'); + } + } + + // Get location logs within a time range + Future> getLocationLogsInRange( + DateTime start, + DateTime end, + ) async { + try { + final response = await _supabase + .from('location_logs') + .select('*, users(*)') + .gte('timestamp', start.toIso8601String()) + .lte('timestamp', end.toIso8601String()) + .order('timestamp', ascending: true); + + final data = response.data as List; + return data.map((json) => LocationLogModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching location logs in range: $e'); + throw Exception('Failed to load location logs in range: $e'); + } + } +} diff --git a/sigap-mobile/lib/src/cores/repositories/map/locations_repository.dart b/sigap-mobile/lib/src/cores/repositories/map/locations_repository.dart index e69de29..e2a28bf 100644 --- a/sigap-mobile/lib/src/cores/repositories/map/locations_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/map/locations_repository.dart @@ -0,0 +1,108 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/map/models/locations_model.dart'; + +class LocationsRepository extends GetxController { + static LocationsRepository get instance => Get.find(); + + final _supabase = SupabaseService.instance.client; + final _log = Get.find(); + + // Get locations by district ID + Future> getLocationsByDistrictId( + String districtId, + ) async { + try { + final response = await _supabase + .from('locations') + .select( + '*, event(*), incident_logs(*), crime_incidents(*), patrol_units(*)', + ) + .eq('district_id', districtId); + + final data = response.data as List; + return data.map((json) => LocationModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching locations for district $districtId: $e'); + throw Exception('Failed to load locations: $e'); + } + } + + // Get location by ID + Future getLocationById(String locationId) async { + try { + final response = + await _supabase + .from('locations') + .select( + '*, event(*), incident_logs(*), crime_incidents(*), patrol_units(*)', + ) + .eq('id', locationId) + .single(); + + return LocationModel.fromJson(response.data); + } catch (e) { + _log.e('Error fetching location $locationId: $e'); + throw Exception('Failed to load location details: $e'); + } + } + + // Get locations within a radius (kilometers) of a point + Future> getNearbyLocations( + double latitude, + double longitude, + double radiusKm, + ) async { + try { + // Use PostGIS to find locations within the radius + final response = await _supabase.rpc( + 'locations_within_radius', + params: { + 'lat': latitude, + 'lng': longitude, + 'radius': radiusKm * 1000, // Convert to meters + }, + ); + + final data = response.data as List; + return data.map((json) => LocationModel.fromJson(json)).toList(); + } catch (e) { + _log.e('Error fetching nearby locations: $e'); + throw Exception('Failed to load nearby locations: $e'); + } + } + + // Add a new location + Future addLocation({ + required String districtId, + required String eventId, + required double latitude, + required double longitude, + String? address, + String? type, + }) async { + try { + final data = { + 'district_id': districtId, + 'event_id': eventId, + 'latitude': latitude, + 'longitude': longitude, + 'address': address, + 'type': type, + }; + + final response = + await _supabase + .from('locations') + .insert(data) + .select('*, event(*)') + .single(); + + return LocationModel.fromJson(response.data); + } catch (e) { + _log.e('Error adding location: $e'); + throw Exception('Failed to add location: $e'); + } + } +} diff --git a/sigap-mobile/lib/src/cores/repositories/personalization/permissions_repository.dart b/sigap-mobile/lib/src/cores/repositories/personalization/permissions_repository.dart index e69de29..85e62dc 100644 --- a/sigap-mobile/lib/src/cores/repositories/personalization/permissions_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/personalization/permissions_repository.dart @@ -0,0 +1,169 @@ +import 'package:get/get.dart'; +import 'package:logger/Logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/personalization/models/permissions_model.dart'; +import 'package:sigap/src/utils/exceptions/exceptions.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class PermissionsRepository extends GetxController { + static PermissionsRepository get instance => Get.find(); + + final _supabase = SupabaseService.instance.client; + final _logger = Get.find(); + + // Get all permissions + Future> getAllPermissions() async { + try { + final permissions = await _supabase + .from('permissions') + .select('*, resource(*), role(*)') + .order('action'); + + return (permissions as List) + .map((permission) => PermissionModel.fromJson(permission)) + .toList(); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in getAllPermissions: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getAllPermissions: $e'); + throw 'Failed to fetch permissions: ${e.toString()}'; + } + } + + // Get permissions by role ID + Future> getPermissionsByRole(String roleId) async { + try { + final permissions = await _supabase + .from('permissions') + .select('*, resource(*), role(*)') + .eq('role_id', roleId); + + return (permissions as List) + .map((permission) => PermissionModel.fromJson(permission)) + .toList(); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in getPermissionsByRole: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getPermissionsByRole: $e'); + throw 'Failed to fetch role permissions: ${e.toString()}'; + } + } + + // Get permissions by resource ID + Future> getPermissionsByResource( + String resourceId, + ) async { + try { + final permissions = await _supabase + .from('permissions') + .select('*, resource(*), role(*)') + .eq('resource_id', resourceId); + + return (permissions as List) + .map((permission) => PermissionModel.fromJson(permission)) + .toList(); + } on PostgrestException catch (error) { + _logger.e( + 'PostgrestException in getPermissionsByResource: ${error.message}', + ); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getPermissionsByResource: $e'); + throw 'Failed to fetch resource permissions: ${e.toString()}'; + } + } + + // Check if the current user has a specific permission + Future checkPermission({ + required String action, + required String resourceName, + }) async { + try { + // Get current user's role ID + final user = _supabase.auth.currentUser; + if (user == null) return false; + + final userData = + await _supabase + .from('users') + .select('roles_id') + .eq('id', user.id) + .single(); + + final roleId = userData['roles_id'] as String; + + // Get resource ID by name + final resourceData = + await _supabase + .from('resources') + .select('id') + .eq('name', resourceName) + .single(); + + final resourceId = resourceData['id'] as String; + + // Check if permission exists + final permissions = await _supabase + .from('permissions') + .select() + .eq('role_id', roleId) + .eq('resource_id', resourceId) + .eq('action', action); + + return (permissions as List).isNotEmpty; + } on PostgrestException catch (error) { + _logger.e('PostgrestException in checkPermission: ${error.message}'); + // If no rows found, return false instead of throwing an exception + if (error.code == 'PGRST116') return false; + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in checkPermission: $e'); + return false; // Default to no permission on error + } + } + + // Create a new permission + Future createPermission({ + required String action, + required String resourceId, + required String roleId, + }) async { + try { + final data = { + 'action': action, + 'resource_id': resourceId, + 'role_id': roleId, + }; + + final createdPermission = + await _supabase + .from('permissions') + .insert(data) + .select('*, resource(*), role(*)') + .single(); + + return PermissionModel.fromJson(createdPermission); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in createPermission: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in createPermission: $e'); + throw 'Failed to create permission: ${e.toString()}'; + } + } + + // Delete a permission + Future deletePermission(String permissionId) async { + try { + await _supabase.from('permissions').delete().eq('id', permissionId); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in deletePermission: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in deletePermission: $e'); + throw 'Failed to delete permission: ${e.toString()}'; + } + } +} diff --git a/sigap-mobile/lib/src/cores/repositories/personalization/profile_repository.dart b/sigap-mobile/lib/src/cores/repositories/personalization/profile_repository.dart index 7b0e3d4..3699aa6 100644 --- a/sigap-mobile/lib/src/cores/repositories/personalization/profile_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/personalization/profile_repository.dart @@ -2,6 +2,8 @@ import 'dart:io'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:logger/Logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; import 'package:sigap/src/features/personalization/models/profile_model.dart'; import 'package:sigap/src/utils/exceptions/exceptions.dart'; import 'package:sigap/src/utils/exceptions/format_exceptions.dart'; @@ -11,7 +13,8 @@ import 'package:supabase_flutter/supabase_flutter.dart'; class ProfileRepository extends GetxController { static ProfileRepository get instance => Get.find(); - final _supabase = Supabase.instance.client; + final _supabase = SupabaseService.instance.client; + final _logger = Get.find(); // Get current user ID String? get currentUserId => _supabase.auth.currentUser?.id; @@ -32,17 +35,19 @@ class ProfileRepository extends GetxController { return ProfileModel.fromJson(profileData); } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in getProfileData: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } on FormatException catch (_) { throw const TFormatException(); } on PlatformException catch (e) { throw TPlatformException(e.code).message; } catch (e) { + _logger.e('Exception in getProfileData: $e'); throw 'Failed to fetch profile data: ${e.toString()}'; } } - // Update avatar + // Upload avatar Future uploadAvatar(String filePath) async { try { if (currentUserId == null) { @@ -51,7 +56,7 @@ class ProfileRepository extends GetxController { final fileName = '${currentUserId}_${DateTime.now().millisecondsSinceEpoch}.jpg'; - final storageResponse = await _supabase.storage + await _supabase.storage .from('avatars') .upload(fileName, File(filePath)); @@ -67,36 +72,109 @@ class ProfileRepository extends GetxController { return avatarUrl; } on StorageException catch (e) { + _logger.e('StorageException in uploadAvatar: ${e.message}'); throw 'Storage error: ${e.message}'; } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in uploadAvatar: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } catch (e) { + _logger.e('Exception in uploadAvatar: $e'); throw 'Failed to upload avatar: ${e.toString()}'; } } - // Update profile information - Future updateProfile(ProfileModel profile) async { + // Update profile + Future updateProfile({ + String? firstName, + String? lastName, + String? bio, + Map? address, + DateTime? birthDate, + String? avatar, + String? username, + }) async { try { if (currentUserId == null) { throw 'User not authenticated'; } - final updatedProfileData = profile.toJson(); + // Build update data object + final Map updateData = {}; - await _supabase - .from('profiles') - .update(updatedProfileData) - .eq('user_id', currentUserId!); + if (firstName != null) updateData['first_name'] = firstName; + if (lastName != null) updateData['last_name'] = lastName; + if (bio != null) updateData['bio'] = bio; + if (address != null) updateData['address'] = address; + if (birthDate != null) + updateData['birth_date'] = birthDate.toIso8601String(); + if (avatar != null) updateData['avatar'] = avatar; + if (username != null) updateData['username'] = username; + // Only update if there's data to update + if (updateData.isNotEmpty) { + await _supabase + .from('profiles') + .update(updateData) + .eq('user_id', currentUserId!); + } + + // Fetch and return updated profile return await getProfileData(); } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in updateProfile: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } catch (e) { + _logger.e('Exception in updateProfile: $e'); throw 'Failed to update profile: ${e.toString()}'; } } + // Create profile if it doesn't exist + Future createProfileIfNotExists({ + required String userId, + required String nik, + String? firstName, + String? lastName, + }) async { + try { + // Check if profile exists + final existingProfiles = await _supabase + .from('profiles') + .select() + .eq('user_id', userId); + + // If profile exists, return it + if ((existingProfiles as List).isNotEmpty) { + return ProfileModel.fromJson(existingProfiles.first); + } + + // Create new profile + final profileData = { + 'user_id': userId, + 'nik': nik, + 'first_name': firstName ?? '', + 'last_name': lastName, + }; + + final createdProfile = + await _supabase + .from('profiles') + .insert(profileData) + .select() + .single(); + + return ProfileModel.fromJson(createdProfile); + } on PostgrestException catch (error) { + _logger.e( + 'PostgrestException in createProfileIfNotExists: ${error.message}', + ); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in createProfileIfNotExists: $e'); + throw 'Failed to create profile: ${e.toString()}'; + } + } + // Get profile by user ID Future getProfileByUserId(String userId) async { try { @@ -109,9 +187,34 @@ class ProfileRepository extends GetxController { return ProfileModel.fromJson(profileData); } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in getProfileByUserId: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } catch (e) { + _logger.e('Exception in getProfileByUserId: $e'); throw 'Failed to fetch profile data: ${e.toString()}'; } } + + // Get profile by NIK + Future getProfileByNIK(String nik) async { + try { + final profiles = + await _supabase + .from('profiles') + .select() + .eq('nik', nik) + .maybeSingle(); + + if (profiles == null) return null; + return ProfileModel.fromJson(profiles); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in getProfileByNIK: ${error.message}'); + // If no rows found, return null instead of throwing an exception + if (error.code == 'PGRST116') return null; + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getProfileByNIK: $e'); + throw 'Failed to fetch profile by NIK: ${e.toString()}'; + } + } } diff --git a/sigap-mobile/lib/src/cores/repositories/personalization/resources_repository.dart b/sigap-mobile/lib/src/cores/repositories/personalization/resources_repository.dart index e69de29..25f0eb7 100644 --- a/sigap-mobile/lib/src/cores/repositories/personalization/resources_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/personalization/resources_repository.dart @@ -0,0 +1,128 @@ +import 'package:get/get.dart'; +import 'package:logger/logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/personalization/models/resources_model.dart'; +import 'package:sigap/src/utils/exceptions/exceptions.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +class ResourcesRepository extends GetxController { + static ResourcesRepository get instance => Get.find(); + + final _supabase = SupabaseService.instance.client; + final _logger = Get.find(); + + // Get all resources + Future> getAllResources() async { + try { + final resources = await _supabase + .from('resources') + .select('*, permissions(*)') + .order('name'); + + return (resources as List) + .map((resource) => ResourceModel.fromJson(resource)) + .toList(); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in getAllResources: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getAllResources: $e'); + throw 'Failed to fetch resources: ${e.toString()}'; + } + } + + // Get resource by ID + Future getResourceById(String resourceId) async { + try { + final resource = + await _supabase + .from('resources') + .select('*, permissions(*)') + .eq('id', resourceId) + .single(); + + return ResourceModel.fromJson(resource); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in getResourceById: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getResourceById: $e'); + throw 'Failed to fetch resource: ${e.toString()}'; + } + } + + // Get resources by type + Future> getResourcesByType(String type) async { + try { + final resources = await _supabase + .from('resources') + .select('*, permissions(*)') + .eq('type', type) + .order('name'); + + return (resources as List) + .map((resource) => ResourceModel.fromJson(resource)) + .toList(); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in getResourcesByType: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getResourcesByType: $e'); + throw 'Failed to fetch resources by type: ${e.toString()}'; + } + } + + // Create a new resource + Future createResource(ResourceModel resource) async { + try { + final createdResource = + await _supabase + .from('resources') + .insert(resource.toJson()) + .select() + .single(); + + return ResourceModel.fromJson(createdResource); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in createResource: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in createResource: $e'); + throw 'Failed to create resource: ${e.toString()}'; + } + } + + // Update a resource + Future updateResource(ResourceModel resource) async { + try { + final updatedResource = + await _supabase + .from('resources') + .update(resource.toJson()) + .eq('id', resource.id) + .select() + .single(); + + return ResourceModel.fromJson(updatedResource); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in updateResource: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in updateResource: $e'); + throw 'Failed to update resource: ${e.toString()}'; + } + } + + // Delete a resource + Future deleteResource(String resourceId) async { + try { + await _supabase.from('resources').delete().eq('id', resourceId); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in deleteResource: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in deleteResource: $e'); + throw 'Failed to delete resource: ${e.toString()}'; + } + } +} diff --git a/sigap-mobile/lib/src/cores/repositories/personalization/roles_repository.dart b/sigap-mobile/lib/src/cores/repositories/personalization/roles_repository.dart index b879a1d..534df28 100644 --- a/sigap-mobile/lib/src/cores/repositories/personalization/roles_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/personalization/roles_repository.dart @@ -1,4 +1,7 @@ import 'package:get/get.dart'; +import 'package:logger/Logger.dart'; +import 'package:sigap/src/cores/services/supabase_service.dart'; +import 'package:sigap/src/features/personalization/models/permissions_model.dart'; import 'package:sigap/src/features/personalization/models/roles_model.dart'; import 'package:sigap/src/utils/exceptions/exceptions.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -6,17 +9,23 @@ import 'package:supabase_flutter/supabase_flutter.dart'; class RolesRepository extends GetxController { static RolesRepository get instance => Get.find(); - final _supabase = Supabase.instance.client; + final _supabase = SupabaseService.instance.client; + final _logger = Get.find(); // Get all roles Future> getAllRoles() async { try { - final roles = await _supabase.from('roles').select().order('name'); + final roles = await _supabase + .from('roles') + .select('*, permissions(*)') + .order('name'); return (roles as List).map((role) => RoleModel.fromJson(role)).toList(); } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in getAllRoles: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } catch (e) { + _logger.e('Exception in getAllRoles: $e'); throw 'Failed to fetch roles: ${e.toString()}'; } } @@ -25,12 +34,18 @@ class RolesRepository extends GetxController { Future getRoleById(String roleId) async { try { final role = - await _supabase.from('roles').select().eq('id', roleId).single(); + await _supabase + .from('roles') + .select('*, permissions(*)') + .eq('id', roleId) + .single(); return RoleModel.fromJson(role); } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in getRoleById: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } catch (e) { + _logger.e('Exception in getRoleById: $e'); throw 'Failed to fetch role data: ${e.toString()}'; } } @@ -39,13 +54,82 @@ class RolesRepository extends GetxController { Future getRoleByName(String roleName) async { try { final role = - await _supabase.from('roles').select().eq('name', roleName).single(); + await _supabase + .from('roles') + .select('*, permissions(*)') + .eq('name', roleName) + .single(); return RoleModel.fromJson(role); } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in getRoleByName: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } catch (e) { + _logger.e('Exception in getRoleByName: $e'); throw 'Failed to fetch role data: ${e.toString()}'; } } + + // Create a new role + Future createRole({ + required String name, + String? description, + String? icon, + }) async { + try { + final data = {'name': name, 'description': description, 'icon': icon}; + + final createdRole = + await _supabase.from('roles').insert(data).select().single(); + + return RoleModel.fromJson(createdRole); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in createRole: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in createRole: $e'); + throw 'Failed to create role: ${e.toString()}'; + } + } + + // Update a role + Future updateRole(RoleModel role) async { + try { + final updatedRole = + await _supabase + .from('roles') + .update(role.toJson()) + .eq('id', role.id) + .select() + .single(); + + return RoleModel.fromJson(updatedRole); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in updateRole: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in updateRole: $e'); + throw 'Failed to update role: ${e.toString()}'; + } + } + + // Get permissions for a role + Future> getRolePermissions(String roleId) async { + try { + final permissions = await _supabase + .from('permissions') + .select('*, resource(*), role(*)') + .eq('role_id', roleId); + + return (permissions as List) + .map((permission) => PermissionModel.fromJson(permission)) + .toList(); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in getRolePermissions: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getRolePermissions: $e'); + throw 'Failed to fetch role permissions: ${e.toString()}'; + } + } } diff --git a/sigap-mobile/lib/src/cores/repositories/personalization/users_repository.dart b/sigap-mobile/lib/src/cores/repositories/personalization/users_repository.dart index e49b394..afb5f93 100644 --- a/sigap-mobile/lib/src/cores/repositories/personalization/users_repository.dart +++ b/sigap-mobile/lib/src/cores/repositories/personalization/users_repository.dart @@ -1,7 +1,9 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:logger/Logger.dart'; import 'package:sigap/src/cores/services/supabase_service.dart'; -import 'package:sigap/src/features/personalization/models/index.dart'; +import 'package:sigap/src/features/personalization/models/roles_model.dart'; +import 'package:sigap/src/features/personalization/models/users_model.dart'; import 'package:sigap/src/utils/exceptions/exceptions.dart'; import 'package:sigap/src/utils/exceptions/format_exceptions.dart'; import 'package:sigap/src/utils/exceptions/platform_exceptions.dart'; @@ -11,14 +13,18 @@ class UserRepository extends GetxController { static UserRepository get instance => Get.find(); final _supabase = SupabaseService.instance.client; + final _logger = Get.find(); // Get current user ID String? get currentUserId => SupabaseService.instance.currentUserId; + // Check if user is authenticated + bool get isAuthenticated => currentUserId != null; + // Check if user is an officer Future isCurrentUserOfficer() async { try { - if (currentUserId == null) return false; + if (!isAuthenticated) return false; final user = _supabase.auth.currentUser; if (user == null) return false; @@ -29,29 +35,30 @@ class UserRepository extends GetxController { return metadata['is_officer'] == true; } - // If no flag in metadata, check if user exists in officers table - final officerData = + // If no flag in metadata, check role information + final userData = await _supabase - .from('officers') - .select() + .from('users') + .select('role:roles(name)') .eq('id', currentUserId!) .single(); - - return officerData != null; + + final roleName = + (userData['role'] as Map)['name'] as String; + return roleName.toLowerCase() == 'officer'; } on PostgrestException catch (error) { - // PGRST116 means no rows returned - not an officer - if (error.code == 'PGRST116') return false; - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in isCurrentUserOfficer: ${error.message}'); + return false; // Default to not an officer } catch (e) { - // Default to not an officer on error - return false; + _logger.e('Exception in isCurrentUserOfficer: $e'); + return false; // Default to not an officer } } - // Get user data for regular users - Future getUserData() async { + // Get current user data + Future getCurrentUserData() async { try { - if (currentUserId == null) { + if (!isAuthenticated) { throw 'User not authenticated'; } @@ -64,70 +71,155 @@ class UserRepository extends GetxController { return UserModel.fromJson(userData); } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in getCurrentUserData: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } on FormatException catch (_) { throw const TFormatException(); } on PlatformException catch (e) { throw TPlatformException(e.code).message; } catch (e) { + _logger.e('Exception in getCurrentUserData: $e'); throw 'Failed to fetch user data: ${e.toString()}'; } } - // Update user profile data - Future updateUserProfile({ - String? firstName, - String? lastName, - String? bio, - String? address, - String? birthDate, - String? avatar, - String? username, - String? phone, - }) async { + // Update user metadata + Future updateUserMetadata(Map metadata) async { try { - if (currentUserId == null) { + if (!isAuthenticated) { throw 'User not authenticated'; } - // First update user profile - final Map profileData = {}; - - if (firstName != null) profileData['first_name'] = firstName; - if (lastName != null) profileData['last_name'] = lastName; - if (bio != null) profileData['bio'] = bio; - if (address != null) profileData['address'] = address; - if (birthDate != null) profileData['birth_date'] = birthDate; - if (avatar != null) profileData['avatar'] = avatar; - if (username != null) profileData['username'] = username; - - if (profileData.isNotEmpty) { - await _supabase - .from('profiles') - .update(profileData) - .eq('user_id', currentUserId!); - } - - // Then update user table if needed - if (phone != null) { - await _supabase - .from('users') - .update({'phone': phone}) - .eq('id', currentUserId!); - - // Also update auth user phone - await _supabase.auth.updateUser(UserAttributes(phone: phone)); - } - } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + await _supabase.auth.updateUser(UserAttributes(data: metadata)); + } on AuthException catch (e) { + _logger.e('AuthException in updateUserMetadata: ${e.message}'); throw TExceptions(e.message); - } on FormatException catch (_) { - throw const TFormatException(); - } on PlatformException catch (e) { - throw TPlatformException(e.code).message; } catch (e) { - throw 'Failed to update profile: ${e.toString()}'; + _logger.e('Exception in updateUserMetadata: $e'); + throw 'Failed to update user metadata: ${e.toString()}'; + } + } + + // Update user email + Future updateUserEmail(String newEmail) async { + try { + if (!isAuthenticated) { + throw 'User not authenticated'; + } + + await _supabase.auth.updateUser(UserAttributes(email: newEmail)); + + // Also update in users table + await _supabase + .from('users') + .update({'email': newEmail}) + .eq('id', currentUserId!); + } on AuthException catch (e) { + _logger.e('AuthException in updateUserEmail: ${e.message}'); + throw TExceptions(e.message); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in updateUserEmail: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in updateUserEmail: $e'); + throw 'Failed to update user email: ${e.toString()}'; + } + } + + // Update user phone + Future updateUserPhone(String newPhone) async { + try { + if (!isAuthenticated) { + throw 'User not authenticated'; + } + + await _supabase.auth.updateUser(UserAttributes(phone: newPhone)); + + // Also update in users table + await _supabase + .from('users') + .update({'phone': newPhone}) + .eq('id', currentUserId!); + + } on AuthException catch (e) { + _logger.e('AuthException in updateUserPhone: ${e.message}'); + throw TExceptions(e.message); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in updateUserPhone: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in updateUserPhone: $e'); + throw 'Failed to update user phone: ${e.toString()}'; + } + } + + // Update user password + Future updateUserPassword(String newPassword) async { + try { + if (!isAuthenticated) { + throw 'User not authenticated'; + } + + await _supabase.auth.updateUser(UserAttributes(password: newPassword)); + + } on AuthException catch (e) { + _logger.e('AuthException in updateUserPassword: ${e.message}'); + throw TExceptions(e.message); + } catch (e) { + _logger.e('Exception in updateUserPassword: $e'); + throw 'Failed to update user password: ${e.toString()}'; + } + } + + // Update user role + Future updateUserRole(String roleId) async { + try { + if (!isAuthenticated) { + throw 'User not authenticated'; + } + + // Update in users table + await _supabase + .from('users') + .update({'roles_id': roleId}) + .eq('id', currentUserId!); + + // Get role details + final roleData = + await _supabase.from('roles').select().eq('id', roleId).single(); + + final role = RoleModel.fromJson(roleData); + + // Update metadata with role information + await updateUserMetadata({ + 'is_officer': role.isOfficer, + 'role_name': role.name, + }); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in updateUserRole: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in updateUserRole: $e'); + throw 'Failed to update user role: ${e.toString()}'; + } + } + + // Get users by role ID + Future> getUsersByRoleId(String roleId) async { + try { + final users = await _supabase + .from('users') + .select('*, profiles(*), role:roles(*)') + .eq('roles_id', roleId); + + return (users as List).map((user) => UserModel.fromJson(user)).toList(); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in getUsersByRoleId: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in getUsersByRoleId: $e'); + throw 'Failed to fetch users by role: ${e.toString()}'; } } @@ -143,9 +235,68 @@ class UserRepository extends GetxController { return UserModel.fromJson(userData); } on PostgrestException catch (error) { - throw TExceptions.fromCode(error.code!); + _logger.e('PostgrestException in getUserById: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); } catch (e) { + _logger.e('Exception in getUserById: $e'); throw 'Failed to fetch user data: ${e.toString()}'; } } + + // Search users by name/username/email + Future> searchUsers(String query, {int limit = 20}) async { + try { + final users = await _supabase + .from('users') + .select('*, profiles(*), role:roles(*)') + .or( + 'email.ilike.%$query%, profiles.first_name.ilike.%$query%, profiles.last_name.ilike.%$query%, profiles.username.ilike.%$query%', + ) + .limit(limit); + + return (users as List).map((user) => UserModel.fromJson(user)).toList(); + } on PostgrestException catch (error) { + _logger.e('PostgrestException in searchUsers: ${error.message}'); + throw TExceptions.fromCode(error.code ?? 'unknown-error'); + } catch (e) { + _logger.e('Exception in searchUsers: $e'); + throw 'Failed to search users: ${e.toString()}'; + } + } + + // Check if user is banned + Future isUserBanned({String? userId}) async { + try { + final id = userId ?? currentUserId; + if (id == null) { + throw 'User ID is required'; + } + + final userData = + await _supabase + .from('users') + .select('is_banned, banned_until') + .eq('id', id) + .single(); + + // Check if explicitly banned + bool isBanned = userData['is_banned'] == true; + + // Check if temporary ban is still active + if (!isBanned && userData['banned_until'] != null) { + final bannedUntil = DateTime.parse(userData['banned_until']); + if (bannedUntil.isAfter(DateTime.now())) { + isBanned = true; + } + } + + return isBanned; + } on PostgrestException catch (error) { + _logger.e('PostgrestException in isUserBanned: ${error.message}'); + return false; // Default to not banned + } catch (e) { + _logger.e('Exception in isUserBanned: $e'); + return false; // Default to not banned + } + } }