feat(panic-button): add models for events, evidences, incident logs, and sessions
- Implement EventModel with JSON serialization and deserialization. - Create EvidenceModel with associated JSON handling. - Develop IncidentLogModel to manage incident logs and their evidences. - Introduce SessionsModel to track user sessions related to events. - Update index file to export new models. feat(personalization): introduce permissions and resources models - Add PermissionModel to manage user permissions with JSON support. - Create ResourceModel to define resources and their associated permissions. - Ensure proper serialization and deserialization for both models. chore(database): update Prisma schema for new models and relationships - Add new models: events, evidences, incident_logs, sessions, permissions, and resources. - Define relationships between users, roles, and permissions. - Update existing models to maintain referential integrity. - Introduce enums for session status, contact message status, crime rates, and crime status.
This commit is contained in:
parent
7ad427baf6
commit
ffed8b8ede
|
@ -0,0 +1,157 @@
|
|||
# 🔐 Auth Verification Flow – GIS Crime App
|
||||
|
||||
Dokumentasi ini merinci alur autentikasi aplikasi termasuk biometric login, verifikasi identitas (NIK/KTP untuk user & NRP/KTA untuk officer), serta sistem anti-spam panic button dan auto-ban.
|
||||
|
||||
---
|
||||
|
||||
## 1. Biometric Login (Fingerprint / Face ID)
|
||||
|
||||
### 📍 Tujuan:
|
||||
|
||||
Mempercepat login pengguna dengan otentikasi biometrik.
|
||||
|
||||
### 🔄 Alur:
|
||||
|
||||
1. App diluncurkan.
|
||||
2. Cek token atau NIK/NRP di `secure_storage`.
|
||||
3. Jika tersedia → munculkan biometric prompt.
|
||||
4. Jika autentikasi sukses → login otomatis ke homepage.
|
||||
5. Jika gagal → redirect ke halaman login manual.
|
||||
|
||||
### 🔧 Teknologi:
|
||||
|
||||
- `local_auth` (Flutter)
|
||||
- `flutter_secure_storage`
|
||||
|
||||
---
|
||||
|
||||
## 2. Registrasi dengan Verifikasi NIK (User) atau NRP (Officer)
|
||||
|
||||
### 📍 Tujuan:
|
||||
|
||||
Mencegah spam akun dan memastikan pengguna adalah individu sah.
|
||||
|
||||
### 🔄 Alur:
|
||||
|
||||
1. User mengisi form registrasi:
|
||||
- Email, password
|
||||
- NIK (user) atau NRP (officer)
|
||||
- Upload foto KTP/KTA (opsional)
|
||||
- Validasi format NIK/NRP
|
||||
- Cek apakah NIK/NRP sudah ada di database.
|
||||
- Jika `is_banned = true` → tolak registrasi.
|
||||
- Jika valid → simpan user baru.
|
||||
|
||||
### 🔧 Teknologi:
|
||||
|
||||
- Supabase/PostgreSQL (unique index + ban flag)
|
||||
- Flutter form + validasi lokal
|
||||
|
||||
---
|
||||
|
||||
## 3. Verifikasi KTP/KTA (Opsional)
|
||||
|
||||
### 📍 Tujuan:
|
||||
|
||||
Validasi lanjutan jika diperlukan (manual atau otomatis).
|
||||
|
||||
### 🅱️ Otomatis (OCR):
|
||||
|
||||
- Gunakan `google_mlkit_text_recognition` dan sejenisnya
|
||||
- Ekstrak teks dari foto KTP atau KTA
|
||||
- Cocokkan dengan data yang dimasukkan
|
||||
|
||||
---
|
||||
|
||||
## 4. Anti-Spam Panic Button + Auto Ban
|
||||
|
||||
### 📍 Tujuan:
|
||||
|
||||
Menghindari penyalahgunaan fitur panic alert.
|
||||
|
||||
### 🔄 Alur:
|
||||
|
||||
1. User menekan panic button.
|
||||
2. Backend:
|
||||
|
||||
- Hitung jumlah panic alert dari user dalam 5 menit terakhir.
|
||||
- Jika > 3 → tambahkan `panic_strike`.
|
||||
- Jika `panic_strike >= 3` → set `is_banned = true`.
|
||||
|
||||
3. Saat user login atau mendaftar:
|
||||
- Cek `is_banned` berdasarkan NIK/NRP.
|
||||
- Jika ya → tolak akses.
|
||||
|
||||
# 📍 Location & Anti-Fake GPS Flow – GIS Crime App
|
||||
|
||||
Dokumentasi ini merinci bagaimana sistem memverifikasi lokasi pengguna dan melindungi fitur penting seperti registrasi dan panic button dari penyalahgunaan melalui fake GPS (location spoofing).
|
||||
|
||||
---
|
||||
|
||||
## teknologi
|
||||
geolocator
|
||||
geocoding
|
||||
|
||||
## 🚦 1. REGISTRASI FLOW (NEW USER)
|
||||
|
||||
### 🔄 Alur:
|
||||
1. 📲 User membuka halaman registrasi
|
||||
2. 📍 Aplikasi meminta akses lokasi
|
||||
3. ✅ Jika lokasi berhasil didapat:
|
||||
- Periksa apakah `position.isMocked == false`
|
||||
- Konversi koordinat → nama kota
|
||||
- Jika kota **"Jember"**:
|
||||
- Lanjutkan proses registrasi
|
||||
- Jika **bukan Jember**:
|
||||
- Tampilkan alert: *"Registrasi hanya tersedia di wilayah Jember."*
|
||||
- Blokir pendaftaran
|
||||
4. ❌ Jika `position.isMocked == true`:
|
||||
- Blokir pendaftaran
|
||||
- Tambahkan flag `spoofing_attempt = true` (opsional)
|
||||
|
||||
---
|
||||
|
||||
## 🚨 2. PANIC BUTTON FLOW
|
||||
|
||||
### 🔄 Alur:
|
||||
1. 📲 User membuka halaman Panic Button
|
||||
2. 📍 Aplikasi:
|
||||
- Ambil posisi GPS terkini
|
||||
- Periksa:
|
||||
- `isMocked == false`
|
||||
- Lokasi berada di wilayah Jember
|
||||
3. ✅ Jika valid:
|
||||
- Panic button aktif
|
||||
4. ❌ Jika:
|
||||
- Lokasi palsu (`isMocked == true`)
|
||||
- Atau lokasi di luar Jember
|
||||
- → Panic button **dinonaktifkan**
|
||||
- Tampilkan alert: *"Fitur Panic tidak dapat digunakan di luar Jember."*
|
||||
5. ⚠️ Jika user mencoba spam dari lokasi palsu:
|
||||
- Tambahkan `spoofing_strike += 1`
|
||||
- Jika `spoofing_strike >= 2` → `is_banned = true`
|
||||
|
||||
---
|
||||
### 🔧 Database Fields (liat schema.prisma untul lebih detail):
|
||||
|
||||
```sql
|
||||
users:
|
||||
.....
|
||||
- is_banned BOOLEAN
|
||||
- ban_reason TEXT
|
||||
- panic_strike INT
|
||||
- profile
|
||||
......
|
||||
- nik
|
||||
|
||||
officer:
|
||||
- nrp TEXT UNIQUE
|
||||
- is_banned BOOLEAN
|
||||
- ban_reason TEXT
|
||||
- panic_strike INT
|
||||
|
||||
panic_logs:
|
||||
- user_id UUID
|
||||
- timestamp TIMESTAMP
|
||||
- location POINT
|
||||
```
|
|
@ -4,6 +4,9 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
|
|||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:sigap/app.dart';
|
||||
import 'package:sigap/src/cores/repositories/panic/panic_button_repository.dart';
|
||||
import 'package:sigap/src/cores/services/biometric_service.dart';
|
||||
import 'package:sigap/src/cores/services/location_service.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
|
@ -32,8 +35,14 @@ Future<void> main() async {
|
|||
storageOptions: const StorageClientOptions(retryAttempts: 10),
|
||||
);
|
||||
|
||||
// Add this to your dependencies initialization:
|
||||
// Initialize services
|
||||
await Get.putAsync(() => SupabaseService().init());
|
||||
await Get.putAsync(() => BiometricService().init());
|
||||
await Get.putAsync(() => LocationService().init());
|
||||
|
||||
// Initialize repositories
|
||||
Get.put(PanicButtonRepository());
|
||||
await Get.find<PanicButtonRepository>().init();
|
||||
|
||||
runApp(const App());
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ import 'package:flutter/services.dart';
|
|||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:get_storage/get_storage.dart';
|
||||
import 'package:sigap/src/cores/services/biometric_service.dart';
|
||||
import 'package:sigap/src/cores/services/location_service.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart';
|
||||
import 'package:sigap/src/features/onboarding/screens/onboarding/onboarding_screen.dart';
|
||||
|
@ -16,17 +18,37 @@ class AuthenticationRepository extends GetxController {
|
|||
// Variable
|
||||
final storage = GetStorage();
|
||||
final _supabase = SupabaseService.instance.client;
|
||||
final _locationService = LocationService.instance;
|
||||
final _biometricService = Get.put(BiometricService());
|
||||
|
||||
// Getters that use the Supabase service
|
||||
User? get authUser => SupabaseService.instance.currentUser;
|
||||
String? get currentUserId => SupabaseService.instance.currentUserId;
|
||||
// get isSessionExpired => authUser?.isExpired;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
FlutterNativeSplash.remove();
|
||||
screenRedirect();
|
||||
// storage.remove('TEMP_ROLE');
|
||||
}
|
||||
|
||||
// Check for biometric login on app start
|
||||
Future<bool> attemptBiometricLogin() async {
|
||||
if (!await _biometricService.isBiometricLoginEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String? sessionString = await _biometricService.attemptBiometricLogin();
|
||||
if (sessionString == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await _supabase.auth.recoverSession(sessionString);
|
||||
Get.offAllNamed('/home');
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
screenRedirect() async {
|
||||
|
@ -38,6 +60,12 @@ class AuthenticationRepository extends GetxController {
|
|||
storage.read('isFirstTime') != true
|
||||
? Get.offAll(() => const SignInScreen())
|
||||
: Get.offAll(() => const OnboardingScreen());
|
||||
} else {
|
||||
// Try biometric login first
|
||||
bool biometricSuccess = await attemptBiometricLogin();
|
||||
if (!biometricSuccess) {
|
||||
Get.offAll(() => const SignInScreen());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,10 +75,29 @@ class AuthenticationRepository extends GetxController {
|
|||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
// Check if the user is banned
|
||||
final bannedUser = await _supabase.rpc(
|
||||
'check_if_banned',
|
||||
params: {'user_email': email},
|
||||
);
|
||||
if (bannedUser != null && bannedUser == true) {
|
||||
throw TExceptions(
|
||||
'This account has been banned due to violation of terms.',
|
||||
);
|
||||
}
|
||||
|
||||
final response = await _supabase.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
// Setup biometric login if available
|
||||
if (_biometricService.isBiometricAvailable.value) {
|
||||
// Ask user if they want to enable biometric login
|
||||
// This would typically be done in the UI, but setting up the flow here
|
||||
await _biometricService.enableBiometricLogin();
|
||||
}
|
||||
|
||||
return response;
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
|
@ -173,10 +220,7 @@ class AuthenticationRepository extends GetxController {
|
|||
) async {
|
||||
try {
|
||||
// Reauthenticate user
|
||||
await _supabase.auth.signInWithPassword(
|
||||
email: email,
|
||||
password: currentPassword,
|
||||
);
|
||||
await _supabase.auth.reauthenticate();
|
||||
|
||||
// Update password
|
||||
await _supabase.auth.updateUser(UserAttributes(password: newPassword));
|
||||
|
@ -314,15 +358,67 @@ class AuthenticationRepository extends GetxController {
|
|||
}
|
||||
}
|
||||
|
||||
// [Email AUTH] - SIGN UP with role selection
|
||||
// [Email AUTH] - SIGN UP with role selection and location verification
|
||||
Future<AuthResponse> signUpWithCredential(
|
||||
String email,
|
||||
String password, {
|
||||
String password,
|
||||
String identifier, // NIK for users or NRP for officers
|
||||
{
|
||||
Map<String, dynamic>? userMetadata,
|
||||
bool isOfficer = false,
|
||||
Map<String, dynamic>? officerData,
|
||||
}) async {
|
||||
try {
|
||||
// Validate location for registration
|
||||
bool isLocationValid = await _locationService.isLocationValidForFeature();
|
||||
if (!isLocationValid) {
|
||||
throw TExceptions('Registration is only available within Jember area. Please ensure your location services are enabled and you are not using a mock location app.');
|
||||
}
|
||||
|
||||
// Check if identifier (NIK/NRP) is already registered
|
||||
final identifierField = isOfficer ? 'nrp' : 'nik';
|
||||
final identifierType = isOfficer ? 'NRP' : 'NIK';
|
||||
|
||||
if (isOfficer) {
|
||||
final existingOfficer = await _supabase
|
||||
.from('officers')
|
||||
.select('id, is_banned')
|
||||
.eq('nrp', identifier)
|
||||
.maybeSingle();
|
||||
|
||||
if (existingOfficer != null) {
|
||||
bool isBanned = existingOfficer['is_banned'] as bool? ?? false;
|
||||
if (isBanned) {
|
||||
throw TExceptions('This $identifierType is associated with a banned account.');
|
||||
} else {
|
||||
throw TExceptions('This $identifierType is already registered.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For regular users, check NIK in profiles
|
||||
final existingUser = await _supabase
|
||||
.from('profiles')
|
||||
.select('id, user_id')
|
||||
.eq('nik', identifier)
|
||||
.maybeSingle();
|
||||
|
||||
if (existingUser != null) {
|
||||
// Check if user is banned
|
||||
final userId = existingUser['user_id'];
|
||||
final userBanCheck = await _supabase
|
||||
.from('users')
|
||||
.select('is_banned')
|
||||
.eq('id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (userBanCheck != null && (userBanCheck['is_banned'] as bool? ?? false)) {
|
||||
throw TExceptions('This $identifierType is associated with a banned account.');
|
||||
} else {
|
||||
throw TExceptions('This $identifierType is already registered.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare the complete user metadata
|
||||
final metadata = userMetadata ?? {};
|
||||
|
||||
|
@ -332,6 +428,16 @@ class AuthenticationRepository extends GetxController {
|
|||
metadata['officer_data'] = officerData;
|
||||
}
|
||||
|
||||
// Add identifier to metadata
|
||||
if (isOfficer) {
|
||||
if (metadata['officer_data'] == null) {
|
||||
metadata['officer_data'] = {};
|
||||
}
|
||||
metadata['officer_data']['nrp'] = identifier;
|
||||
} else {
|
||||
metadata['nik'] = identifier;
|
||||
}
|
||||
|
||||
final authResponse = await _supabase.auth.signUp(
|
||||
email: email,
|
||||
password: password,
|
||||
|
@ -345,6 +451,14 @@ class AuthenticationRepository extends GetxController {
|
|||
throw TExceptions('Failed to sign up. Please try again.');
|
||||
}
|
||||
|
||||
// Create profile with NIK if user is not an officer
|
||||
if (!isOfficer && user != null) {
|
||||
await _supabase.from('profiles').insert({
|
||||
'user_id': user.id,
|
||||
'nik': identifier,
|
||||
});
|
||||
}
|
||||
|
||||
return authResponse;
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
|
@ -355,10 +469,32 @@ class AuthenticationRepository extends GetxController {
|
|||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
if (e is TExceptions) {
|
||||
rethrow;
|
||||
}
|
||||
throw TExceptions('Something went wrong. Please try again later.');
|
||||
}
|
||||
}
|
||||
|
||||
// Enable or disable biometric login
|
||||
Future<void> toggleBiometricLogin(bool enable) async {
|
||||
if (enable) {
|
||||
await _biometricService.enableBiometricLogin();
|
||||
} else {
|
||||
await _biometricService.disableBiometricLogin();
|
||||
}
|
||||
}
|
||||
|
||||
// Check if biometric login is enabled
|
||||
Future<bool> isBiometricLoginEnabled() async {
|
||||
return await _biometricService.isBiometricLoginEnabled();
|
||||
}
|
||||
|
||||
// Check if biometrics are available on the device
|
||||
Future<bool> isBiometricAvailable() async {
|
||||
return _biometricService.isBiometricAvailable.value;
|
||||
}
|
||||
|
||||
// ----------------- Logout -----------------
|
||||
// [Sign Out] - SIGN OUT
|
||||
Future<void> signOut() async {
|
||||
|
@ -382,7 +518,7 @@ class AuthenticationRepository extends GetxController {
|
|||
Future<void> deleteAccount() async {
|
||||
try {
|
||||
final userId = _supabase.auth.currentUser!.id;
|
||||
await _supabase.rpc('delete_user', params: {'user_id': userId});
|
||||
await _supabase.rpc('delete_account', params: {'user_id': userId});
|
||||
} on AuthException catch (e) {
|
||||
throw TExceptions(e.message);
|
||||
} on FormatException catch (_) {
|
||||
|
|
|
@ -0,0 +1,446 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/services/location_service.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class PanicButtonRepository extends GetxController {
|
||||
static PanicButtonRepository get instance => Get.find();
|
||||
|
||||
final _supabase = SupabaseService.instance.client;
|
||||
final _locationService = LocationService.instance;
|
||||
|
||||
// Rate limiting
|
||||
final RxInt recentPanicCount = 0.obs;
|
||||
final RxBool isRateLimited = false.obs;
|
||||
final RxInt panicStrikeCount = 0.obs;
|
||||
|
||||
// Constants
|
||||
static const int maxPanicAlertsIn5Minutes = 3;
|
||||
static const int strikesTillBan = 3;
|
||||
|
||||
// Initialize
|
||||
Future<void> init() async {
|
||||
await _loadPanicStrikeCount();
|
||||
_startRateLimitCooldown();
|
||||
}
|
||||
|
||||
// Send a panic alert
|
||||
Future<bool> sendPanicAlert({
|
||||
required String description,
|
||||
required String categoryId,
|
||||
String? mediaUrl,
|
||||
}) async {
|
||||
try {
|
||||
final userId = SupabaseService.instance.currentUserId;
|
||||
if (userId == null) {
|
||||
throw TExceptions('User not logged in.');
|
||||
}
|
||||
|
||||
// Check if user is banned
|
||||
bool isBanned = await _checkIfUserIsBanned();
|
||||
if (isBanned) {
|
||||
throw TExceptions('Your account has been temporarily banned from using the panic button due to misuse.');
|
||||
}
|
||||
|
||||
// Check location validity
|
||||
if (!await _locationService.isLocationValidForFeature()) {
|
||||
_incrementSpoofingAttempts();
|
||||
throw TExceptions('Location validation failed. Please ensure you are in Jember and not using a mock location app.');
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
if (isRateLimited.value || recentPanicCount.value >= maxPanicAlertsIn5Minutes) {
|
||||
_incrementPanicStrike();
|
||||
throw TExceptions('You have reached the maximum number of panic alerts. Please try again later.');
|
||||
}
|
||||
|
||||
// Get current location
|
||||
final position = await _locationService.getCurrentPosition();
|
||||
if (position == null) {
|
||||
throw TExceptions('Failed to get current location.');
|
||||
}
|
||||
|
||||
// Create a new incident log
|
||||
final incidentId = await _createIncidentLog(
|
||||
userId: userId,
|
||||
latitude: position.latitude,
|
||||
longitude: position.longitude,
|
||||
description: description,
|
||||
categoryId: categoryId,
|
||||
);
|
||||
|
||||
// Create panic button log
|
||||
await _createPanicButtonLog(
|
||||
userId: userId,
|
||||
incidentId: incidentId,
|
||||
);
|
||||
|
||||
// Add media if provided
|
||||
if (mediaUrl != null && mediaUrl.isNotEmpty) {
|
||||
await _addEvidenceToIncident(incidentId, mediaUrl);
|
||||
}
|
||||
|
||||
// Increment panic count for rate limiting
|
||||
recentPanicCount.value++;
|
||||
_checkRateLimit();
|
||||
|
||||
return true;
|
||||
} on TExceptions {
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to send panic alert: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Create an incident log
|
||||
Future<String> _createIncidentLog({
|
||||
required String userId,
|
||||
required double latitude,
|
||||
required double longitude,
|
||||
required String description,
|
||||
required String categoryId,
|
||||
}) async {
|
||||
try {
|
||||
// First create a location
|
||||
final locationResponse = await _supabase.rpc('create_location', params: {
|
||||
'lat': latitude,
|
||||
'long': longitude,
|
||||
'district_id': null, // Will be filled by database function
|
||||
'event_id': null, // Will be filled by database function
|
||||
});
|
||||
|
||||
final locationId = locationResponse as String;
|
||||
|
||||
// Then create incident log
|
||||
final incidentResponse = await _supabase.from('incident_logs').insert({
|
||||
'user_id': userId,
|
||||
'location_id': locationId,
|
||||
'category_id': categoryId,
|
||||
'description': description,
|
||||
'source': 'panic_button',
|
||||
'time': DateTime.now().toIso8601String(),
|
||||
'verified': false,
|
||||
}).select('id').single();
|
||||
|
||||
return incidentResponse['id'] as String;
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to create incident log: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Create panic button log
|
||||
Future<void> _createPanicButtonLog({
|
||||
required String userId,
|
||||
required String incidentId,
|
||||
}) async {
|
||||
try {
|
||||
// Get officer ID if the user is an officer
|
||||
String? officerId;
|
||||
final user = SupabaseService.instance.currentUser;
|
||||
if (user != null && user.userMetadata?['is_officer'] == true) {
|
||||
final officerQuery = await _supabase
|
||||
.from('officers')
|
||||
.select('id')
|
||||
.eq('nrp', user.userMetadata?['officer_data']?['nrp'])
|
||||
.maybeSingle();
|
||||
|
||||
if (officerQuery != null) {
|
||||
officerId = officerQuery['id'] as String;
|
||||
}
|
||||
}
|
||||
|
||||
await _supabase.from('panic_button_logs').insert({
|
||||
'user_id': userId,
|
||||
'incident_id': incidentId,
|
||||
'officer_id': officerId,
|
||||
'timestamp': DateTime.now().toIso8601String(),
|
||||
});
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to create panic button log: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Add evidence to incident
|
||||
Future<void> _addEvidenceToIncident(String incidentId, String mediaUrl) async {
|
||||
try {
|
||||
await _supabase.from('evidence').insert({
|
||||
'id': 'EV-${DateTime.now().millisecondsSinceEpoch}',
|
||||
'incident_id': incidentId,
|
||||
'type': 'photo',
|
||||
'url': mediaUrl,
|
||||
'uploaded_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
} catch (e) {
|
||||
// Non-critical error, just log it
|
||||
print('Failed to add evidence: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Check rate limit
|
||||
void _checkRateLimit() {
|
||||
if (recentPanicCount.value >= maxPanicAlertsIn5Minutes) {
|
||||
isRateLimited.value = true;
|
||||
_incrementPanicStrike();
|
||||
}
|
||||
}
|
||||
|
||||
// Start cooldown for rate limiting
|
||||
void _startRateLimitCooldown() {
|
||||
// Reset panic count every 5 minutes
|
||||
Future.delayed(const Duration(minutes: 5), () {
|
||||
recentPanicCount.value = 0;
|
||||
isRateLimited.value = false;
|
||||
_startRateLimitCooldown();
|
||||
});
|
||||
}
|
||||
|
||||
// Increment panic strike
|
||||
Future<void> _incrementPanicStrike() async {
|
||||
try {
|
||||
final userId = SupabaseService.instance.currentUserId;
|
||||
if (userId == null) return;
|
||||
|
||||
// Check if user is officer
|
||||
final user = SupabaseService.instance.currentUser;
|
||||
bool isOfficer = user?.userMetadata?['is_officer'] == true;
|
||||
|
||||
// Increment strike in the appropriate table
|
||||
if (isOfficer) {
|
||||
final officerQuery = await _supabase
|
||||
.from('officers')
|
||||
.select('id, nrp, panic_strike')
|
||||
.eq('nrp', user?.userMetadata?['officer_data']?['nrp'])
|
||||
.single();
|
||||
|
||||
int currentStrikes = officerQuery['panic_strike'] as int;
|
||||
panicStrikeCount.value = currentStrikes + 1;
|
||||
|
||||
await _supabase
|
||||
.from('officers')
|
||||
.update({
|
||||
'panic_strike': panicStrikeCount.value,
|
||||
'is_banned': panicStrikeCount.value >= strikesTillBan,
|
||||
'banned_reason': panicStrikeCount.value >= strikesTillBan
|
||||
? 'Exceeded panic button usage limit'
|
||||
: null,
|
||||
'banned_until': panicStrikeCount.value >= strikesTillBan
|
||||
? DateTime.now().add(const Duration(days: 3)).toIso8601String()
|
||||
: null,
|
||||
})
|
||||
.eq('id', officerQuery['id']);
|
||||
} else {
|
||||
// Regular user
|
||||
final userQuery = await _supabase
|
||||
.from('users')
|
||||
.select('panic_strike')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
int currentStrikes = userQuery['panic_strike'] as int;
|
||||
panicStrikeCount.value = currentStrikes + 1;
|
||||
|
||||
await _supabase
|
||||
.from('users')
|
||||
.update({
|
||||
'panic_strike': panicStrikeCount.value,
|
||||
'is_banned': panicStrikeCount.value >= strikesTillBan,
|
||||
'banned_reason': panicStrikeCount.value >= strikesTillBan
|
||||
? 'Exceeded panic button usage limit'
|
||||
: null,
|
||||
'banned_until': panicStrikeCount.value >= strikesTillBan
|
||||
? DateTime.now().add(const Duration(days: 3)).toIso8601String()
|
||||
: null,
|
||||
})
|
||||
.eq('id', userId);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Failed to update panic strike: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Increment spoofing attempts
|
||||
Future<void> _incrementSpoofingAttempts() async {
|
||||
try {
|
||||
final userId = SupabaseService.instance.currentUserId;
|
||||
if (userId == null) return;
|
||||
|
||||
// Check if user is officer
|
||||
final user = SupabaseService.instance.currentUser;
|
||||
bool isOfficer = user?.userMetadata?['is_officer'] == true;
|
||||
|
||||
// Increment spoofing attempts in the appropriate table
|
||||
if (isOfficer) {
|
||||
final officerQuery = await _supabase
|
||||
.from('officers')
|
||||
.select('id, nrp, spoofing_attempts')
|
||||
.eq('nrp', user?.userMetadata?['officer_data']?['nrp'])
|
||||
.single();
|
||||
|
||||
int currentAttempts = officerQuery['spoofing_attempts'] as int;
|
||||
int newAttempts = currentAttempts + 1;
|
||||
|
||||
await _supabase
|
||||
.from('officers')
|
||||
.update({
|
||||
'spoofing_attempts': newAttempts,
|
||||
'is_banned': newAttempts >= 2,
|
||||
'banned_reason': newAttempts >= 2
|
||||
? 'Suspected location spoofing'
|
||||
: null,
|
||||
'banned_until': newAttempts >= 2
|
||||
? DateTime.now().add(const Duration(days: 7)).toIso8601String()
|
||||
: null,
|
||||
})
|
||||
.eq('id', officerQuery['id']);
|
||||
} else {
|
||||
// Regular user
|
||||
final userQuery = await _supabase
|
||||
.from('users')
|
||||
.select('spoofing_attempts')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
int currentAttempts = userQuery['spoofing_attempts'] as int;
|
||||
int newAttempts = currentAttempts + 1;
|
||||
|
||||
await _supabase
|
||||
.from('users')
|
||||
.update({
|
||||
'spoofing_attempts': newAttempts,
|
||||
'is_banned': newAttempts >= 2,
|
||||
'banned_reason': newAttempts >= 2
|
||||
? 'Suspected location spoofing'
|
||||
: null,
|
||||
'banned_until': newAttempts >= 2
|
||||
? DateTime.now().add(const Duration(days: 7)).toIso8601String()
|
||||
: null,
|
||||
})
|
||||
.eq('id', userId);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Failed to update spoofing attempts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user is banned
|
||||
Future<bool> _checkIfUserIsBanned() async {
|
||||
try {
|
||||
final userId = SupabaseService.instance.currentUserId;
|
||||
if (userId == null) return false;
|
||||
|
||||
// Check if user is officer
|
||||
final user = SupabaseService.instance.currentUser;
|
||||
bool isOfficer = user?.userMetadata?['is_officer'] == true;
|
||||
|
||||
if (isOfficer) {
|
||||
final officerQuery = await _supabase
|
||||
.from('officers')
|
||||
.select('is_banned, banned_until')
|
||||
.eq('nrp', user?.userMetadata?['officer_data']?['nrp'])
|
||||
.maybeSingle();
|
||||
|
||||
if (officerQuery == null) return false;
|
||||
|
||||
bool isBanned = officerQuery['is_banned'] as bool? ?? false;
|
||||
String? bannedUntil = officerQuery['banned_until'] as String?;
|
||||
|
||||
// Check if ban has expired
|
||||
if (isBanned && bannedUntil != null) {
|
||||
DateTime banEnd = DateTime.parse(bannedUntil);
|
||||
if (DateTime.now().isAfter(banEnd)) {
|
||||
// Ban has expired, remove it
|
||||
await _supabase
|
||||
.from('officers')
|
||||
.update({
|
||||
'is_banned': false,
|
||||
'banned_until': null,
|
||||
'banned_reason': null,
|
||||
})
|
||||
.eq('nrp', user?.userMetadata?['officer_data']?['nrp']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return isBanned;
|
||||
} else {
|
||||
// Regular user
|
||||
final userQuery = await _supabase
|
||||
.from('users')
|
||||
.select('is_banned, banned_until')
|
||||
.eq('id', userId)
|
||||
.single();
|
||||
|
||||
bool isBanned = userQuery['is_banned'] as bool? ?? false;
|
||||
String? bannedUntil = userQuery['banned_until'] as String?;
|
||||
|
||||
// Check if ban has expired
|
||||
if (isBanned && bannedUntil != null) {
|
||||
DateTime banEnd = DateTime.parse(bannedUntil);
|
||||
if (DateTime.now().isAfter(banEnd)) {
|
||||
// Ban has expired, remove it
|
||||
await _supabase
|
||||
.from('users')
|
||||
.update({
|
||||
'is_banned': false,
|
||||
'banned_until': null,
|
||||
'banned_reason': null,
|
||||
})
|
||||
.eq('id', userId);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return isBanned;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Failed to check ban status: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load panic strike count
|
||||
Future<void> _loadPanicStrikeCount() async {
|
||||
try {
|
||||
final userId = SupabaseService.instance.currentUserId;
|
||||
if (userId == null) {
|
||||
panicStrikeCount.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if user is officer
|
||||
final user = SupabaseService.instance.currentUser;
|
||||
bool isOfficer = user?.userMetadata?['is_officer'] == true;
|
||||
|
||||
if (isOfficer) {
|
||||
final officerQuery = await _supabase
|
||||
.from('officers')
|
||||
.select('panic_strike')
|
||||
.eq('nrp', user?.userMetadata?['officer_data']?['nrp'])
|
||||
.maybeSingle();
|
||||
|
||||
if (officerQuery != null) {
|
||||
panicStrikeCount.value = officerQuery['panic_strike'] as int? ?? 0;
|
||||
} else {
|
||||
panicStrikeCount.value = 0;
|
||||
}
|
||||
} else {
|
||||
// Regular user
|
||||
final userQuery = await _supabase
|
||||
.from('users')
|
||||
.select('panic_strike')
|
||||
.eq('id', userId)
|
||||
.maybeSingle();
|
||||
|
||||
if (userQuery != null) {
|
||||
panicStrikeCount.value = userQuery['panic_strike'] as int? ?? 0;
|
||||
} else {
|
||||
panicStrikeCount.value = 0;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
panicStrikeCount.value = 0;
|
||||
print('Failed to load panic strike count: $e');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/features/daily-ops/models/patrol_units_model.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class PatrolUnitRepository extends GetxController {
|
||||
static PatrolUnitRepository get instance => Get.find();
|
||||
|
||||
final _supabase = SupabaseService.instance.client;
|
||||
|
||||
// Get all patrol units
|
||||
Future<List<PatrolUnitModel>> getAllPatrolUnits() async {
|
||||
try {
|
||||
final patrolUnits = await _supabase
|
||||
.from('patrol_units')
|
||||
.select('*, members:officers(*)')
|
||||
.order('name');
|
||||
|
||||
return patrolUnits.map((unit) => PatrolUnitModel.fromJson(unit)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch patrol units: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get patrol unit by ID
|
||||
Future<PatrolUnitModel> getPatrolUnitById(String id) async {
|
||||
try {
|
||||
final patrolUnit = await _supabase
|
||||
.from('patrol_units')
|
||||
.select('*, members:officers(*)')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
return PatrolUnitModel.fromJson(patrolUnit);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch patrol unit data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get patrol units by unit ID
|
||||
Future<List<PatrolUnitModel>> getPatrolUnitsByUnitId(String unitId) async {
|
||||
try {
|
||||
final patrolUnits = await _supabase
|
||||
.from('patrol_units')
|
||||
.select('*, members:officers(*)')
|
||||
.eq('unit_id', unitId)
|
||||
.order('name');
|
||||
|
||||
return patrolUnits.map((unit) => PatrolUnitModel.fromJson(unit)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch patrol units: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get patrol units by status
|
||||
Future<List<PatrolUnitModel>> getPatrolUnitsByStatus(String status) async {
|
||||
try {
|
||||
final patrolUnits = await _supabase
|
||||
.from('patrol_units')
|
||||
.select('*, members:officers(*)')
|
||||
.eq('status', status)
|
||||
.order('name');
|
||||
|
||||
return patrolUnits.map((unit) => PatrolUnitModel.fromJson(unit)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch patrol units: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get patrol units by type
|
||||
Future<List<PatrolUnitModel>> getPatrolUnitsByType(String type) async {
|
||||
try {
|
||||
final patrolUnits = await _supabase
|
||||
.from('patrol_units')
|
||||
.select('*, members:officers(*)')
|
||||
.eq('type', type)
|
||||
.order('name');
|
||||
|
||||
return patrolUnits.map((unit) => PatrolUnitModel.fromJson(unit)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch patrol units: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Update patrol unit status
|
||||
Future<void> updatePatrolUnitStatus(String id, String status) async {
|
||||
try {
|
||||
await _supabase
|
||||
.from('patrol_units')
|
||||
.update({'status': status})
|
||||
.eq('id', id);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to update patrol unit status: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/features/daily-ops/models/units_model.dart';
|
||||
import 'package:sigap/src/features/daily-ops/models/patrol_units_model.dart';
|
||||
import 'package:sigap/src/features/daily-ops/models/unit_statistics_model.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class UnitRepository extends GetxController {
|
||||
static UnitRepository get instance => Get.find();
|
||||
|
||||
final _supabase = SupabaseService.instance.client;
|
||||
|
||||
// Get all units
|
||||
Future<List<UnitModel>> getAllUnits() async {
|
||||
try {
|
||||
final units = await _supabase
|
||||
.from('units')
|
||||
.select()
|
||||
.order('name');
|
||||
|
||||
return units.map((unit) => UnitModel.fromJson(unit)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch units: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get unit by ID
|
||||
Future<UnitModel> getUnitById(String codeUnit) async {
|
||||
try {
|
||||
final unit = await _supabase
|
||||
.from('units')
|
||||
.select('*, officers(*), patrol_units(*), unit_statistics(*)')
|
||||
.eq('code_unit', codeUnit)
|
||||
.single();
|
||||
|
||||
return UnitModel.fromJson(unit);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch unit data: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get units by type
|
||||
Future<List<UnitModel>> getUnitsByType(UnitType type) async {
|
||||
try {
|
||||
final units = await _supabase
|
||||
.from('units')
|
||||
.select()
|
||||
.eq('type', type.name)
|
||||
.order('name');
|
||||
|
||||
return units.map((unit) => UnitModel.fromJson(unit)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch units by type: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get units by city
|
||||
Future<List<UnitModel>> getUnitsByCity(String cityId) async {
|
||||
try {
|
||||
final units = await _supabase
|
||||
.from('units')
|
||||
.select()
|
||||
.eq('city_id', cityId)
|
||||
.order('name');
|
||||
|
||||
return units.map((unit) => UnitModel.fromJson(unit)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch units by city: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get units near a location
|
||||
Future<List<UnitModel>> getUnitsNearLocation(double latitude, double longitude, double radiusInKm) async {
|
||||
try {
|
||||
// Use PostGIS to find units within a radius
|
||||
final units = await _supabase
|
||||
.rpc('get_units_near_location', params: {
|
||||
'lat': latitude,
|
||||
'lng': longitude,
|
||||
'radius_km': radiusInKm,
|
||||
});
|
||||
|
||||
return units.map((unit) => UnitModel.fromJson(unit)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch nearby units: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/features/daily-ops/models/unit_statistics_model.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class UnitStatisticsRepository extends GetxController {
|
||||
static UnitStatisticsRepository get instance => Get.find();
|
||||
|
||||
final _supabase = SupabaseService.instance.client;
|
||||
|
||||
// Get statistics for a specific unit
|
||||
Future<List<UnitStatisticModel>> getUnitStatistics(String codeUnit) async {
|
||||
try {
|
||||
final statistics = await _supabase
|
||||
.from('unit_statistics')
|
||||
.select()
|
||||
.eq('code_unit', codeUnit)
|
||||
.order('year', ascending: false)
|
||||
.order('month', ascending: false);
|
||||
|
||||
return statistics.map((stat) => UnitStatisticModel.fromJson(stat)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch unit statistics: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get statistics for a specific year
|
||||
Future<List<UnitStatisticModel>> getUnitStatisticsByYear(String codeUnit, int year) async {
|
||||
try {
|
||||
final statistics = await _supabase
|
||||
.from('unit_statistics')
|
||||
.select()
|
||||
.eq('code_unit', codeUnit)
|
||||
.eq('year', year)
|
||||
.order('month');
|
||||
|
||||
return statistics.map((stat) => UnitStatisticModel.fromJson(stat)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch unit statistics: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get statistics for a specific month and year
|
||||
Future<UnitStatisticModel> getUnitStatisticByMonthYear(String codeUnit, int month, int year) async {
|
||||
try {
|
||||
final statistic = await _supabase
|
||||
.from('unit_statistics')
|
||||
.select()
|
||||
.eq('code_unit', codeUnit)
|
||||
.eq('month', month)
|
||||
.eq('year', year)
|
||||
.single();
|
||||
|
||||
return UnitStatisticModel.fromJson(statistic);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch unit statistic: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get latest statistics for all units
|
||||
Future<List<UnitStatisticModel>> getLatestStatisticsForAllUnits() async {
|
||||
try {
|
||||
// This would need a custom RPC function in Supabase to get the latest statistic for each unit
|
||||
final statistics = await _supabase.rpc('get_latest_unit_statistics');
|
||||
|
||||
return statistics.map((stat) => UnitStatisticModel.fromJson(stat)).toList();
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch latest statistics: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Get total statistics summarized by year
|
||||
Future<List<Map<String, dynamic>>> getTotalStatisticsByYear(int year) async {
|
||||
try {
|
||||
final statistics = await _supabase.rpc('summarize_statistics_by_year', params: {'target_year': year});
|
||||
|
||||
return List<Map<String, dynamic>>.from(statistics);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to fetch statistics summary: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:local_auth/local_auth.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
|
||||
class BiometricService extends GetxService {
|
||||
static BiometricService get instance => Get.find<BiometricService>();
|
||||
|
||||
final LocalAuthentication _localAuth = LocalAuthentication();
|
||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||
|
||||
final RxBool isBiometricAvailable = false.obs;
|
||||
final RxBool isAuthenticated = false.obs;
|
||||
|
||||
// Keys for secure storage
|
||||
static const String _biometricEnabledKey = 'biometric_enabled';
|
||||
static const String _userIdKey = 'user_id';
|
||||
static const String _sessionKey = 'session';
|
||||
static const String _identifierKey = 'identifier'; // NIK or NRP
|
||||
|
||||
// Initialize the biometric service
|
||||
Future<BiometricService> init() async {
|
||||
await _checkBiometrics();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Check if biometric authentication is available
|
||||
Future<bool> _checkBiometrics() async {
|
||||
try {
|
||||
isBiometricAvailable.value =
|
||||
await _localAuth.canCheckBiometrics &&
|
||||
await _localAuth.isDeviceSupported();
|
||||
return isBiometricAvailable.value;
|
||||
} on PlatformException catch (_) {
|
||||
isBiometricAvailable.value = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get available biometric types
|
||||
Future<List<BiometricType>> getAvailableBiometrics() async {
|
||||
try {
|
||||
return await _localAuth.getAvailableBiometrics();
|
||||
} on PlatformException catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Request biometric authentication
|
||||
Future<bool> authenticate({
|
||||
String reason = 'Authenticate to continue',
|
||||
}) async {
|
||||
try {
|
||||
if (!isBiometricAvailable.value) {
|
||||
await _checkBiometrics();
|
||||
if (!isBiometricAvailable.value) return false;
|
||||
}
|
||||
|
||||
isAuthenticated.value = await _localAuth.authenticate(
|
||||
localizedReason: reason,
|
||||
options: const AuthenticationOptions(
|
||||
stickyAuth: true,
|
||||
biometricOnly: true,
|
||||
),
|
||||
);
|
||||
|
||||
return isAuthenticated.value;
|
||||
} on PlatformException catch (_) {
|
||||
isAuthenticated.value = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Enable biometric login for the current user
|
||||
Future<void> enableBiometricLogin() async {
|
||||
final user = SupabaseService.instance.currentUser;
|
||||
if (user == null) return;
|
||||
|
||||
await _secureStorage.write(key: _biometricEnabledKey, value: 'true');
|
||||
await _secureStorage.write(key: _userIdKey, value: user.id);
|
||||
|
||||
// Store the session for auto-login
|
||||
final session = SupabaseService.instance.client.auth.currentSession;
|
||||
if (session != null) {
|
||||
await _secureStorage.write(
|
||||
key: _sessionKey,
|
||||
value: session.toJson().toString(),
|
||||
);
|
||||
}
|
||||
|
||||
// Store identifier (NIK or NRP) based on user role
|
||||
final userMetadata = user.userMetadata;
|
||||
String? identifier;
|
||||
|
||||
if (userMetadata != null) {
|
||||
if (userMetadata['is_officer'] == true &&
|
||||
userMetadata['officer_data'] != null) {
|
||||
identifier = userMetadata['officer_data']['nrp'];
|
||||
} else if (userMetadata['nik'] != null) {
|
||||
identifier = userMetadata['nik'];
|
||||
}
|
||||
}
|
||||
|
||||
if (identifier != null) {
|
||||
await _secureStorage.write(key: _identifierKey, value: identifier);
|
||||
}
|
||||
}
|
||||
|
||||
// Disable biometric login
|
||||
Future<void> disableBiometricLogin() async {
|
||||
await _secureStorage.delete(key: _biometricEnabledKey);
|
||||
await _secureStorage.delete(key: _userIdKey);
|
||||
await _secureStorage.delete(key: _sessionKey);
|
||||
await _secureStorage.delete(key: _identifierKey);
|
||||
}
|
||||
|
||||
// Check if biometric login is enabled
|
||||
Future<bool> isBiometricLoginEnabled() async {
|
||||
final enabled = await _secureStorage.read(key: _biometricEnabledKey);
|
||||
return enabled == 'true';
|
||||
}
|
||||
|
||||
// Attempt to perform biometric login
|
||||
Future<String?> attemptBiometricLogin() async {
|
||||
if (!await isBiometricLoginEnabled()) return null;
|
||||
|
||||
final success = await authenticate(reason: 'Log in to your account');
|
||||
if (!success) return null;
|
||||
|
||||
return await _secureStorage.read(key: _sessionKey);
|
||||
}
|
||||
|
||||
// Get stored user identifier (NIK or NRP)
|
||||
Future<String?> getStoredIdentifier() async {
|
||||
return await _secureStorage.read(key: _identifierKey);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:geocoding/geocoding.dart';
|
||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||
|
||||
class LocationService extends GetxService {
|
||||
static LocationService get instance => Get.find<LocationService>();
|
||||
|
||||
final RxBool isLocationServiceEnabled = false.obs;
|
||||
final RxBool isPermissionGranted = false.obs;
|
||||
final Rx<Position?> currentPosition = Rx<Position?>(null);
|
||||
final RxString currentCity = ''.obs;
|
||||
final RxBool isMockedLocation = false.obs;
|
||||
|
||||
// Jember's center coordinate (approximate)
|
||||
static const double jemberLatitude = -8.168333;
|
||||
static const double jemberLongitude = 113.702778;
|
||||
|
||||
// Max distance from Jember in meters (approximately 30km radius)
|
||||
static const double maxDistanceFromJember = 30000;
|
||||
|
||||
// Initialize the service
|
||||
Future<LocationService> init() async {
|
||||
await _checkLocationService();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Check if location service is enabled and permission is granted
|
||||
Future<bool> _checkLocationService() async {
|
||||
try {
|
||||
// Check if location service is enabled
|
||||
isLocationServiceEnabled.value = await Geolocator.isLocationServiceEnabled();
|
||||
if (!isLocationServiceEnabled.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check location permission
|
||||
var permission = await Geolocator.checkPermission();
|
||||
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
isPermissionGranted.value = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (permission == LocationPermission.deniedForever) {
|
||||
isPermissionGranted.value = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
isPermissionGranted.value = true;
|
||||
return true;
|
||||
} catch (e) {
|
||||
isLocationServiceEnabled.value = false;
|
||||
isPermissionGranted.value = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Request location permission
|
||||
Future<bool> requestLocationPermission() async {
|
||||
try {
|
||||
var permission = await Geolocator.requestPermission();
|
||||
isPermissionGranted.value = permission == LocationPermission.always ||
|
||||
permission == LocationPermission.whileInUse;
|
||||
return isPermissionGranted.value;
|
||||
} catch (e) {
|
||||
isPermissionGranted.value = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current position
|
||||
Future<Position?> getCurrentPosition() async {
|
||||
try {
|
||||
if (!isPermissionGranted.value) {
|
||||
bool hasPermission = await _checkLocationService();
|
||||
if (!hasPermission) return null;
|
||||
}
|
||||
|
||||
currentPosition.value = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
timeLimit: const Duration(seconds: 10),
|
||||
);
|
||||
|
||||
// Check if location is mocked
|
||||
isMockedLocation.value = currentPosition.value?.isMocked ?? false;
|
||||
|
||||
// Get city name from coordinates
|
||||
if (currentPosition.value != null) {
|
||||
await _updateCityName();
|
||||
}
|
||||
|
||||
return currentPosition.value;
|
||||
} catch (e) {
|
||||
throw TExceptions('Failed to get location: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
// Update the city name based on current position
|
||||
Future<void> _updateCityName() async {
|
||||
if (currentPosition.value == null) return;
|
||||
|
||||
try {
|
||||
List<Placemark> placemarks = await placemarkFromCoordinates(
|
||||
currentPosition.value!.latitude,
|
||||
currentPosition.value!.longitude,
|
||||
);
|
||||
|
||||
if (placemarks.isNotEmpty) {
|
||||
currentCity.value = placemarks.first.locality ?? '';
|
||||
}
|
||||
} catch (e) {
|
||||
currentCity.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user is in Jember
|
||||
bool isInJember() {
|
||||
if (currentPosition.value == null) return false;
|
||||
|
||||
// First check by city name if available
|
||||
if (currentCity.value.toLowerCase().contains('jember')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Then check by distance from Jember's center
|
||||
double distanceInMeters = Geolocator.distanceBetween(
|
||||
currentPosition.value!.latitude,
|
||||
currentPosition.value!.longitude,
|
||||
jemberLatitude,
|
||||
jemberLongitude,
|
||||
);
|
||||
|
||||
return distanceInMeters <= maxDistanceFromJember;
|
||||
}
|
||||
|
||||
// Check if location is valid for registration or panic button
|
||||
Future<bool> isLocationValidForFeature() async {
|
||||
await getCurrentPosition();
|
||||
|
||||
if (currentPosition.value == null) return false;
|
||||
|
||||
// Check if location is mocked
|
||||
if (isMockedLocation.value) return false;
|
||||
|
||||
// Check if in Jember
|
||||
return isInJember();
|
||||
}
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||
import 'package:sigap/src/features/daily-ops/models/units_model.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
||||
class StepFormController extends GetxController {
|
||||
|
@ -11,6 +13,7 @@ class StepFormController extends GetxController {
|
|||
// Role information
|
||||
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
|
||||
|
||||
|
||||
// Current step index
|
||||
final RxInt currentStep = 0.obs;
|
||||
|
||||
|
@ -23,7 +26,7 @@ class StepFormController extends GetxController {
|
|||
final addressController = TextEditingController();
|
||||
|
||||
// Viewer-specific fields
|
||||
final emergencyNameController = TextEditingController();
|
||||
final nikController = TextEditingController();
|
||||
final emergencyPhoneController = TextEditingController();
|
||||
final relationshipController = TextEditingController();
|
||||
|
||||
|
@ -39,7 +42,7 @@ class StepFormController extends GetxController {
|
|||
final RxString addressError = ''.obs;
|
||||
|
||||
// Error states - Viewer
|
||||
final RxString emergencyNameError = ''.obs;
|
||||
final RxString nikError = ''.obs;
|
||||
final RxString emergencyPhoneError = ''.obs;
|
||||
final RxString relationshipError = ''.obs;
|
||||
|
||||
|
@ -53,8 +56,7 @@ class StepFormController extends GetxController {
|
|||
final RxBool isLoading = false.obs;
|
||||
|
||||
// Available units for officer role
|
||||
final RxList<Map<String, dynamic>> availableUnits =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final RxList<UnitModel> availableUnits = <UnitModel>[].obs;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
|
@ -94,18 +96,11 @@ class StepFormController extends GetxController {
|
|||
// Here we would fetch units from repository
|
||||
// For now we'll use dummy data
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
availableUnits.value = [
|
||||
{"id": "unit1", "name": "Polres Jember"},
|
||||
{"id": "unit2", "name": "Polsek Sumbersari"},
|
||||
{"id": "unit3", "name": "Polsek Kaliwates"},
|
||||
];
|
||||
|
||||
} catch (e) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Failed to load units: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'Error',
|
||||
message: 'Failed to fetch available units: ${e.toString()}',
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
|
@ -119,7 +114,7 @@ class StepFormController extends GetxController {
|
|||
phoneController.dispose();
|
||||
addressController.dispose();
|
||||
|
||||
emergencyNameController.dispose();
|
||||
nikController.dispose();
|
||||
emergencyPhoneController.dispose();
|
||||
relationshipController.dispose();
|
||||
|
||||
|
@ -165,7 +160,7 @@ class StepFormController extends GetxController {
|
|||
addressError.value = '';
|
||||
|
||||
// Clear viewer-specific errors
|
||||
emergencyNameError.value = '';
|
||||
nikError.value = '';
|
||||
emergencyPhoneError.value = '';
|
||||
relationshipError.value = '';
|
||||
|
||||
|
@ -215,11 +210,11 @@ class StepFormController extends GetxController {
|
|||
|
||||
final nameValidation = TValidators.validateUserInput(
|
||||
'Emergency contact name',
|
||||
emergencyNameController.text,
|
||||
nikController.text,
|
||||
100,
|
||||
);
|
||||
if (nameValidation != null) {
|
||||
emergencyNameError.value = nameValidation;
|
||||
nikError.value = nameValidation;
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
|
@ -376,7 +371,7 @@ class StepFormController extends GetxController {
|
|||
} else {
|
||||
// Viewer role
|
||||
final emergencyContact = {
|
||||
'name': emergencyNameController.text,
|
||||
'name': nikController.text,
|
||||
'phone': emergencyPhoneController.text,
|
||||
'relationship': relationshipController.text,
|
||||
};
|
||||
|
|
|
@ -116,7 +116,7 @@ class EmailVerificationScreen extends StatelessWidget {
|
|||
() => TextButton(
|
||||
onPressed:
|
||||
controller.isResendEnabled.value
|
||||
? controller.resendCode
|
||||
? () => controller.resendCode
|
||||
: null,
|
||||
child: Text(
|
||||
controller.isResendEnabled.value
|
||||
|
|
|
@ -248,15 +248,15 @@ class StepFormScreen extends StatelessWidget {
|
|||
// Emergency contact name field
|
||||
Obx(
|
||||
() => CustomTextField(
|
||||
label: 'Contact Name',
|
||||
controller: controller.emergencyNameController,
|
||||
label: 'NIK',
|
||||
controller: controller.nikController,
|
||||
validator:
|
||||
(value) => TValidators.validateUserInput(
|
||||
'Emergency contact name',
|
||||
value,
|
||||
100,
|
||||
),
|
||||
errorText: controller.emergencyNameError.value,
|
||||
errorText: controller.nikError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
),
|
||||
),
|
||||
|
@ -377,8 +377,8 @@ class StepFormScreen extends StatelessWidget {
|
|||
controller.availableUnits
|
||||
.map(
|
||||
(unit) => DropdownMenuItem(
|
||||
value: unit['id'],
|
||||
child: Text(unit['name']),
|
||||
value: unit.codeUnit,
|
||||
child: Text(unit.name),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
|
@ -391,7 +391,7 @@ class StepFormScreen extends StatelessWidget {
|
|||
controller.unitIdController.text = value.toString();
|
||||
}
|
||||
},
|
||||
validator: TValidators.validateUnitId,
|
||||
validator: (value) => TValidators.validateUnitId(value),
|
||||
errorText: controller.unitIdError.value,
|
||||
),
|
||||
),
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export 'patrol_units_model.dart';
|
||||
export 'unit_statistics_model.dart';
|
||||
export 'units_model.dart';
|
|
@ -0,0 +1,112 @@
|
|||
import 'package:sigap/src/features/daily-ops/models/units_model.dart';
|
||||
import 'package:sigap/src/features/map/models/locations_model.dart';
|
||||
import 'package:sigap/src/features/personalization/models/officers_model.dart';
|
||||
|
||||
class PatrolUnitModel {
|
||||
final String id;
|
||||
final String unitId;
|
||||
final String locationId;
|
||||
final String name;
|
||||
final String type;
|
||||
final String status;
|
||||
final double radius;
|
||||
final DateTime createdAt;
|
||||
final List<OfficerModel>? members;
|
||||
final LocationModel? location;
|
||||
final UnitModel? unit;
|
||||
|
||||
PatrolUnitModel({
|
||||
required this.id,
|
||||
required this.unitId,
|
||||
required this.locationId,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.status,
|
||||
required this.radius,
|
||||
required this.createdAt,
|
||||
this.members,
|
||||
this.location,
|
||||
this.unit,
|
||||
});
|
||||
|
||||
// Create a PatrolUnitModel instance from a JSON object
|
||||
factory PatrolUnitModel.fromJson(Map<String, dynamic> json) {
|
||||
return PatrolUnitModel(
|
||||
id: json['id'] as String,
|
||||
unitId: json['unit_id'] as String,
|
||||
locationId: json['location_id'] as String,
|
||||
name: json['name'] as String,
|
||||
type: json['type'] as String,
|
||||
status: json['status'] as String,
|
||||
radius: json['radius'].toDouble(),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
members:
|
||||
json['members'] != null
|
||||
? (json['members'] as List)
|
||||
.map((e) => OfficerModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
location:
|
||||
json['location'] != null
|
||||
? LocationModel.fromJson(json['location'] as Map<String, dynamic>)
|
||||
: null,
|
||||
unit:
|
||||
json['unit'] != null
|
||||
? UnitModel.fromJson(json['unit'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a PatrolUnitModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'unit_id': unitId,
|
||||
'location_id': locationId,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'status': status,
|
||||
'radius': radius,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
if (members != null) 'members': members!.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the PatrolUnitModel with updated fields
|
||||
PatrolUnitModel copyWith({
|
||||
String? id,
|
||||
String? unitId,
|
||||
String? locationId,
|
||||
String? name,
|
||||
String? type,
|
||||
String? status,
|
||||
double? radius,
|
||||
DateTime? createdAt,
|
||||
List<OfficerModel>? members,
|
||||
LocationModel? location,
|
||||
UnitModel? unit,
|
||||
}) {
|
||||
return PatrolUnitModel(
|
||||
id: id ?? this.id,
|
||||
unitId: unitId ?? this.unitId,
|
||||
locationId: locationId ?? this.locationId,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
status: status ?? this.status,
|
||||
radius: radius ?? this.radius,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
members: members ?? this.members,
|
||||
location: location ?? this.location,
|
||||
unit: unit ?? this.unit,
|
||||
);
|
||||
}
|
||||
|
||||
get coordinates {
|
||||
return {'latitude': location?.latitude, 'longitude': location?.longitude};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PatrolUnitModel(id: $id, name: $name, status: $status)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
class UnitStatisticModel {
|
||||
final String id;
|
||||
final int crimeTotal;
|
||||
final int crimeCleared;
|
||||
final double? percentage;
|
||||
final int? pending;
|
||||
final int month;
|
||||
final int year;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String codeUnit;
|
||||
|
||||
UnitStatisticModel({
|
||||
required this.id,
|
||||
required this.crimeTotal,
|
||||
required this.crimeCleared,
|
||||
this.percentage,
|
||||
this.pending,
|
||||
required this.month,
|
||||
required this.year,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
required this.codeUnit,
|
||||
});
|
||||
|
||||
// Create a UnitStatisticModel instance from a JSON object
|
||||
factory UnitStatisticModel.fromJson(Map<String, dynamic> json) {
|
||||
return UnitStatisticModel(
|
||||
id: json['id'] as String,
|
||||
crimeTotal: json['crime_total'] as int,
|
||||
crimeCleared: json['crime_cleared'] as int,
|
||||
percentage: json['percentage']?.toDouble(),
|
||||
pending: json['pending'] as int?,
|
||||
month: json['month'] as int,
|
||||
year: json['year'] as int,
|
||||
createdAt: json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'] as String)
|
||||
: null,
|
||||
updatedAt: json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'] as String)
|
||||
: null,
|
||||
codeUnit: json['code_unit'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a UnitStatisticModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'crime_total': crimeTotal,
|
||||
'crime_cleared': crimeCleared,
|
||||
'percentage': percentage,
|
||||
'pending': pending,
|
||||
'month': month,
|
||||
'year': year,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
'code_unit': codeUnit,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the UnitStatisticModel with updated fields
|
||||
UnitStatisticModel copyWith({
|
||||
String? id,
|
||||
int? crimeTotal,
|
||||
int? crimeCleared,
|
||||
double? percentage,
|
||||
int? pending,
|
||||
int? month,
|
||||
int? year,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? codeUnit,
|
||||
}) {
|
||||
return UnitStatisticModel(
|
||||
id: id ?? this.id,
|
||||
crimeTotal: crimeTotal ?? this.crimeTotal,
|
||||
crimeCleared: crimeCleared ?? this.crimeCleared,
|
||||
percentage: percentage ?? this.percentage,
|
||||
pending: pending ?? this.pending,
|
||||
month: month ?? this.month,
|
||||
year: year ?? this.year,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
codeUnit: codeUnit ?? this.codeUnit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UnitStatisticModel(id: $id, month: $month, year: $year, crimeTotal: $crimeTotal, crimeCleared: $crimeCleared)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
import 'package:sigap/src/features/daily-ops/models/patrol_units_model.dart';
|
||||
import 'package:sigap/src/features/daily-ops/models/unit_statistics_model.dart';
|
||||
import 'package:sigap/src/features/personalization/models/officers_model.dart';
|
||||
|
||||
enum UnitType { polda, polsek, polres, other }
|
||||
|
||||
class UnitModel {
|
||||
final String codeUnit;
|
||||
final String? districtId;
|
||||
final String name;
|
||||
final String? description;
|
||||
final UnitType type;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? address;
|
||||
final double? landArea;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final String cityId;
|
||||
final String? phone;
|
||||
final List<OfficerModel>? officers;
|
||||
final List<PatrolUnitModel>? patrolUnits;
|
||||
final List<UnitStatisticModel>? statistics;
|
||||
|
||||
UnitModel({
|
||||
required this.codeUnit,
|
||||
this.districtId,
|
||||
required this.name,
|
||||
this.description,
|
||||
required this.type,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.address,
|
||||
this.landArea,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.cityId,
|
||||
this.phone,
|
||||
this.officers,
|
||||
this.patrolUnits,
|
||||
this.statistics,
|
||||
});
|
||||
|
||||
// Create a UnitModel instance from a JSON object
|
||||
factory UnitModel.fromJson(Map<String, dynamic> json) {
|
||||
return UnitModel(
|
||||
codeUnit: json['code_unit'] as String,
|
||||
districtId: json['district_id'] as String?,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
type: _parseUnitType(json['type'] as String),
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
address: json['address'] as String?,
|
||||
landArea: json['land_area']?.toDouble(),
|
||||
latitude: json['latitude'].toDouble(),
|
||||
longitude: json['longitude'].toDouble(),
|
||||
cityId: json['city_id'] as String,
|
||||
phone: json['phone'] as String?,
|
||||
officers:
|
||||
json['officers'] != null
|
||||
? (json['officers'] as List)
|
||||
.map((e) => OfficerModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
patrolUnits:
|
||||
json['patrol_units'] != null
|
||||
? (json['patrol_units'] as List)
|
||||
.map(
|
||||
(e) => PatrolUnitModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
statistics:
|
||||
json['unit_statistics'] != null
|
||||
? (json['unit_statistics'] as List)
|
||||
.map(
|
||||
(e) =>
|
||||
UnitStatisticModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a UnitModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'code_unit': codeUnit,
|
||||
'district_id': districtId,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'type': type.name,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
'address': address,
|
||||
'land_area': landArea,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'city_id': cityId,
|
||||
'phone': phone,
|
||||
if (officers != null)
|
||||
'officers': officers!.map((e) => e.toJson()).toList(),
|
||||
if (patrolUnits != null)
|
||||
'patrol_units': patrolUnits!.map((e) => e.toJson()).toList(),
|
||||
if (statistics != null)
|
||||
'unit_statistics': statistics!.map((e) => e.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the UnitModel with updated fields
|
||||
UnitModel copyWith({
|
||||
String? codeUnit,
|
||||
String? districtId,
|
||||
String? name,
|
||||
String? description,
|
||||
UnitType? type,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? address,
|
||||
double? landArea,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
String? cityId,
|
||||
String? phone,
|
||||
List<OfficerModel>? officers,
|
||||
List<PatrolUnitModel>? patrolUnits,
|
||||
List<UnitStatisticModel>? statistics,
|
||||
}) {
|
||||
return UnitModel(
|
||||
codeUnit: codeUnit ?? this.codeUnit,
|
||||
districtId: districtId ?? this.districtId,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
type: type ?? this.type,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
address: address ?? this.address,
|
||||
landArea: landArea ?? this.landArea,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
cityId: cityId ?? this.cityId,
|
||||
phone: phone ?? this.phone,
|
||||
officers: officers ?? this.officers,
|
||||
patrolUnits: patrolUnits ?? this.patrolUnits,
|
||||
statistics: statistics ?? this.statistics,
|
||||
);
|
||||
}
|
||||
|
||||
static UnitType _parseUnitType(String type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'polda':
|
||||
return UnitType.polda;
|
||||
case 'polsek':
|
||||
return UnitType.polsek;
|
||||
case 'polres':
|
||||
return UnitType.polres;
|
||||
default:
|
||||
return UnitType.other;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UnitModel(codeUnit: $codeUnit, name: $name, type: ${type.name})';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
import 'package:sigap/src/features/daily-ops/models/units_model.dart';
|
||||
import 'package:sigap/src/features/map/models/districts_model.dart';
|
||||
|
||||
class CityModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final List<DistrictModel>? districts;
|
||||
final List<UnitModel>? units;
|
||||
|
||||
CityModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.districts,
|
||||
this.units,
|
||||
});
|
||||
|
||||
// Create a CityModel instance from a JSON object
|
||||
factory CityModel.fromJson(Map<String, dynamic> json) {
|
||||
return CityModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
districts:
|
||||
json['districts'] != null
|
||||
? (json['districts'] as List)
|
||||
.map((e) => DistrictModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
units:
|
||||
json['units'] != null
|
||||
? (json['units'] as List)
|
||||
.map((e) => UnitModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a CityModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the CityModel with updated fields
|
||||
CityModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<DistrictModel>? districts,
|
||||
List<UnitModel>? units,
|
||||
}) {
|
||||
return CityModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
districts: districts ?? this.districts,
|
||||
units: units ?? this.units,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CityModel(id: $id, name: $name)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:sigap/src/features/map/models/districts_model.dart';
|
||||
|
||||
class DemographicModel {
|
||||
final String id;
|
||||
final String districtId;
|
||||
final int population;
|
||||
final int numberOfUnemployed;
|
||||
final double populationDensity;
|
||||
final int year;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DistrictModel? district;
|
||||
|
||||
DemographicModel({
|
||||
required this.id,
|
||||
required this.districtId,
|
||||
required this.population,
|
||||
required this.numberOfUnemployed,
|
||||
required this.populationDensity,
|
||||
required this.year,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.district,
|
||||
});
|
||||
|
||||
// Create a DemographicModel instance from a JSON object
|
||||
factory DemographicModel.fromJson(Map<String, dynamic> json) {
|
||||
return DemographicModel(
|
||||
id: json['id'] as String,
|
||||
districtId: json['district_id'] as String,
|
||||
population: json['population'] as int,
|
||||
numberOfUnemployed: json['number_of_unemployed'] as int,
|
||||
populationDensity: json['population_density'].toDouble(),
|
||||
year: json['year'] as int,
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
district:
|
||||
json['districts'] != null
|
||||
? DistrictModel.fromJson(
|
||||
json['districts'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a DemographicModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'district_id': districtId,
|
||||
'population': population,
|
||||
'number_of_unemployed': numberOfUnemployed,
|
||||
'population_density': populationDensity,
|
||||
'year': year,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the DemographicModel with updated fields
|
||||
DemographicModel copyWith({
|
||||
String? id,
|
||||
String? districtId,
|
||||
int? population,
|
||||
int? numberOfUnemployed,
|
||||
double? populationDensity,
|
||||
int? year,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DistrictModel? district,
|
||||
}) {
|
||||
return DemographicModel(
|
||||
id: id ?? this.id,
|
||||
districtId: districtId ?? this.districtId,
|
||||
population: population ?? this.population,
|
||||
numberOfUnemployed: numberOfUnemployed ?? this.numberOfUnemployed,
|
||||
populationDensity: populationDensity ?? this.populationDensity,
|
||||
year: year ?? this.year,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
district: district ?? this.district,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DemographicModel(id: $id, districtId: $districtId, year: $year)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
import 'package:sigap/src/features/daily-ops/models/units_model.dart';
|
||||
import 'package:sigap/src/features/map/models/cities_model.dart';
|
||||
import 'package:sigap/src/features/map/models/demographics_model.dart';
|
||||
import 'package:sigap/src/features/map/models/geographics_model.dart';
|
||||
import 'package:sigap/src/features/map/models/locations_model.dart';
|
||||
import 'package:sigap/src/features/panic-button/models/crimes_model.dart';
|
||||
|
||||
class DistrictModel {
|
||||
final String id;
|
||||
final String cityId;
|
||||
final String name;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final List<CrimeModel>? crimes;
|
||||
final List<DemographicModel>? demographics;
|
||||
final CityModel? city;
|
||||
final List<GeographicModel>? geographics;
|
||||
final List<LocationModel>? locations;
|
||||
final UnitModel? unit;
|
||||
|
||||
DistrictModel({
|
||||
required this.id,
|
||||
required this.cityId,
|
||||
required this.name,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.crimes,
|
||||
this.demographics,
|
||||
this.city,
|
||||
this.geographics,
|
||||
this.locations,
|
||||
this.unit,
|
||||
});
|
||||
|
||||
// Create a DistrictModel instance from a JSON object
|
||||
factory DistrictModel.fromJson(Map<String, dynamic> json) {
|
||||
return DistrictModel(
|
||||
id: json['id'] as String,
|
||||
cityId: json['city_id'] as String,
|
||||
name: json['name'] as String,
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
crimes:
|
||||
json['crimes'] != null
|
||||
? (json['crimes'] as List)
|
||||
.map((e) => CrimeModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
demographics:
|
||||
json['demographics'] != null
|
||||
? (json['demographics'] as List)
|
||||
.map(
|
||||
(e) => DemographicModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
city:
|
||||
json['cities'] != null
|
||||
? CityModel.fromJson(json['cities'] as Map<String, dynamic>)
|
||||
: null,
|
||||
geographics:
|
||||
json['geographics'] != null
|
||||
? (json['geographics'] as List)
|
||||
.map(
|
||||
(e) => GeographicModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
locations:
|
||||
json['locations'] != null
|
||||
? (json['locations'] as List)
|
||||
.map((e) => LocationModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
unit:
|
||||
json['units'] != null
|
||||
? UnitModel.fromJson(json['units'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a DistrictModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'city_id': cityId,
|
||||
'name': name,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the DistrictModel with updated fields
|
||||
DistrictModel copyWith({
|
||||
String? id,
|
||||
String? cityId,
|
||||
String? name,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<CrimeModel>? crimes,
|
||||
List<DemographicModel>? demographics,
|
||||
CityModel? city,
|
||||
List<GeographicModel>? geographics,
|
||||
List<LocationModel>? locations,
|
||||
UnitModel? unit,
|
||||
}) {
|
||||
return DistrictModel(
|
||||
id: id ?? this.id,
|
||||
cityId: cityId ?? this.cityId,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
crimes: crimes ?? this.crimes,
|
||||
demographics: demographics ?? this.demographics,
|
||||
city: city ?? this.city,
|
||||
geographics: geographics ?? this.geographics,
|
||||
locations: locations ?? this.locations,
|
||||
unit: unit ?? this.unit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'DistrictModel(id: $id, name: $name, cityId: $cityId)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
import 'package:sigap/src/features/map/models/districts_model.dart';
|
||||
|
||||
class GeographicModel {
|
||||
final String id;
|
||||
final String districtId;
|
||||
final String? address;
|
||||
final double longitude;
|
||||
final double latitude;
|
||||
final double? landArea;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? description;
|
||||
final String? type;
|
||||
final int? year;
|
||||
final DistrictModel? district;
|
||||
|
||||
GeographicModel({
|
||||
required this.id,
|
||||
required this.districtId,
|
||||
this.address,
|
||||
required this.longitude,
|
||||
required this.latitude,
|
||||
this.landArea,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.description,
|
||||
this.type,
|
||||
this.year,
|
||||
this.district,
|
||||
});
|
||||
|
||||
// Create a GeographicModel instance from a JSON object
|
||||
factory GeographicModel.fromJson(Map<String, dynamic> json) {
|
||||
return GeographicModel(
|
||||
id: json['id'] as String,
|
||||
districtId: json['district_id'] as String,
|
||||
address: json['address'] as String?,
|
||||
longitude: json['longitude'].toDouble(),
|
||||
latitude: json['latitude'].toDouble(),
|
||||
landArea: json['land_area']?.toDouble(),
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
description: json['description'] as String?,
|
||||
type: json['type'] as String?,
|
||||
year: json['year'] as int?,
|
||||
district:
|
||||
json['districts'] != null
|
||||
? DistrictModel.fromJson(
|
||||
json['districts'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a GeographicModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'district_id': districtId,
|
||||
'address': address,
|
||||
'longitude': longitude,
|
||||
'latitude': latitude,
|
||||
'land_area': landArea,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
'description': description,
|
||||
'type': type,
|
||||
'year': year,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the GeographicModel with updated fields
|
||||
GeographicModel copyWith({
|
||||
String? id,
|
||||
String? districtId,
|
||||
String? address,
|
||||
double? longitude,
|
||||
double? latitude,
|
||||
double? landArea,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? description,
|
||||
String? type,
|
||||
int? year,
|
||||
DistrictModel? district,
|
||||
}) {
|
||||
return GeographicModel(
|
||||
id: id ?? this.id,
|
||||
districtId: districtId ?? this.districtId,
|
||||
address: address ?? this.address,
|
||||
longitude: longitude ?? this.longitude,
|
||||
latitude: latitude ?? this.latitude,
|
||||
landArea: landArea ?? this.landArea,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
description: description ?? this.description,
|
||||
type: type ?? this.type,
|
||||
year: year ?? this.year,
|
||||
district: district ?? this.district,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'GeographicModel(id: $id, districtId: $districtId, type: $type)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export 'cities_model.dart';
|
||||
export 'demographics_model.dart';
|
||||
export 'districts_model.dart';
|
||||
export 'geographics_model.dart';
|
||||
export 'location_logs_model.dart';
|
||||
export 'locations_model.dart';
|
|
@ -0,0 +1,87 @@
|
|||
import 'package:sigap/src/features/personalization/models/users_model.dart';
|
||||
|
||||
class LocationLogModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final DateTime timestamp;
|
||||
final String? description;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final UserModel? user;
|
||||
|
||||
LocationLogModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.timestamp,
|
||||
this.description,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.user,
|
||||
});
|
||||
|
||||
// Create a LocationLogModel instance from a JSON object
|
||||
factory LocationLogModel.fromJson(Map<String, dynamic> json) {
|
||||
return LocationLogModel(
|
||||
id: json['id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
latitude: json['latitude'].toDouble(),
|
||||
longitude: json['longitude'].toDouble(),
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
description: json['description'] as String?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
user:
|
||||
json['users'] != null
|
||||
? UserModel.fromJson(json['users'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a LocationLogModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'user_id': userId,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'description': description,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the LocationLogModel with updated fields
|
||||
LocationLogModel copyWith({
|
||||
String? id,
|
||||
String? userId,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
DateTime? timestamp,
|
||||
String? description,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
UserModel? user,
|
||||
}) {
|
||||
return LocationLogModel(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
description: description ?? this.description,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
user: user ?? this.user,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LocationLogModel(id: $id, userId: $userId, timestamp: $timestamp)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
import 'package:sigap/src/features/daily-ops/models/patrol_units_model.dart';
|
||||
import 'package:sigap/src/features/panic-button/models/crime_incidents_model.dart';
|
||||
import 'package:sigap/src/features/panic-button/models/events_model.dart';
|
||||
import 'package:sigap/src/features/panic-button/models/incident_logs_model.dart';
|
||||
|
||||
class LocationModel {
|
||||
final String id;
|
||||
final String districtId;
|
||||
final String eventId;
|
||||
final String? address;
|
||||
final String? type;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final double? landArea;
|
||||
final double? distanceToUnit;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
// Relations
|
||||
final EventModel event;
|
||||
final List<CrimeIncidentModel>? crimeIncidents;
|
||||
final List<IncidentLogModel>? incidentLogs;
|
||||
final List<PatrolUnitModel>? patrolUnits;
|
||||
|
||||
LocationModel({
|
||||
required this.id,
|
||||
required this.districtId,
|
||||
required this.eventId,
|
||||
this.address,
|
||||
this.type,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
this.landArea,
|
||||
this.distanceToUnit,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
required this.event,
|
||||
this.crimeIncidents,
|
||||
this.incidentLogs,
|
||||
this.patrolUnits,
|
||||
});
|
||||
|
||||
// Create a LocationModel instance from a JSON object
|
||||
factory LocationModel.fromJson(Map<String, dynamic> json) {
|
||||
return LocationModel(
|
||||
id: json['id'] as String,
|
||||
districtId: json['district_id'] as String,
|
||||
eventId: json['event_id'] as String,
|
||||
address: json['address'] as String?,
|
||||
type: json['type'] as String?,
|
||||
latitude: json['latitude'].toDouble(),
|
||||
longitude: json['longitude'].toDouble(),
|
||||
landArea: json['land_area']?.toDouble(),
|
||||
distanceToUnit: json['distance_to_unit']?.toDouble(),
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
event: EventModel.fromJson(json['event'] as Map<String, dynamic>),
|
||||
crimeIncidents:
|
||||
json['crime_incidents'] != null
|
||||
? (json['crime_incidents'] as List)
|
||||
.map(
|
||||
(e) =>
|
||||
CrimeIncidentModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
incidentLogs:
|
||||
json['incident_logs'] != null
|
||||
? (json['incident_logs'] as List)
|
||||
.map(
|
||||
(e) => IncidentLogModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
patrolUnits:
|
||||
json['patrol_units'] != null
|
||||
? (json['patrol_units'] as List)
|
||||
.map(
|
||||
(e) => PatrolUnitModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a LocationModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'district_id': districtId,
|
||||
'event_id': eventId,
|
||||
'address': address,
|
||||
'type': type,
|
||||
'latitude': latitude,
|
||||
'longitude': longitude,
|
||||
'land_area': landArea,
|
||||
'distance_to_unit': distanceToUnit,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the LocationModel with updated fields
|
||||
LocationModel copyWith({
|
||||
String? id,
|
||||
String? districtId,
|
||||
String? eventId,
|
||||
String? address,
|
||||
String? type,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
double? landArea,
|
||||
double? distanceToUnit,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
EventModel? event,
|
||||
List<CrimeIncidentModel>? crimeIncidents,
|
||||
List<IncidentLogModel>? incidentLogs,
|
||||
List<PatrolUnitModel>? patrolUnits,
|
||||
}) {
|
||||
return LocationModel(
|
||||
id: id ?? this.id,
|
||||
districtId: districtId ?? this.districtId,
|
||||
eventId: eventId ?? this.eventId,
|
||||
address: address ?? this.address,
|
||||
type: type ?? this.type,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
landArea: landArea ?? this.landArea,
|
||||
distanceToUnit: distanceToUnit ?? this.distanceToUnit,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
event: event ?? this.event,
|
||||
crimeIncidents: crimeIncidents ?? this.crimeIncidents,
|
||||
incidentLogs: incidentLogs ?? this.incidentLogs,
|
||||
patrolUnits: patrolUnits ?? this.patrolUnits,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'LocationModel(id: $id, latitude: $latitude, longitude: $longitude)';
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import 'package:get/get.dart';
|
||||
import 'package:sigap/src/cores/repositories/roles/roles_repository.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/features/personalization/models/roles_model.dart';
|
||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/features/personalization/models/index.dart';
|
||||
import 'package:sigap/src/features/personalization/models/roles_model.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
||||
class RoleCard extends StatelessWidget {
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
enum CrimeStatus { open, closed, underInvestigation, resolved, unresolved }
|
||||
|
||||
class CrimeIncidentModel {
|
||||
final String id;
|
||||
final String crimeId;
|
||||
final String crimeCategoryId;
|
||||
final String locationId;
|
||||
final String description;
|
||||
final int victimCount;
|
||||
final CrimeStatus? status;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final DateTime timestamp;
|
||||
|
||||
CrimeIncidentModel({
|
||||
required this.id,
|
||||
required this.crimeId,
|
||||
required this.crimeCategoryId,
|
||||
required this.locationId,
|
||||
required this.description,
|
||||
required this.victimCount,
|
||||
this.status = CrimeStatus.open,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
// Create a CrimeIncidentModel instance from a JSON object
|
||||
factory CrimeIncidentModel.fromJson(Map<String, dynamic> json) {
|
||||
return CrimeIncidentModel(
|
||||
id: json['id'] as String,
|
||||
crimeId: json['crime_id'] as String,
|
||||
crimeCategoryId: json['crime_category_id'] as String,
|
||||
locationId: json['location_id'] as String,
|
||||
description: json['description'] as String,
|
||||
victimCount: json['victim_count'] as int,
|
||||
status: _parseStatus(json['status']?.toString()),
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
timestamp: DateTime.parse(json['timestamp']),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a CrimeIncidentModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'crime_id': crimeId,
|
||||
'crime_category_id': crimeCategoryId,
|
||||
'location_id': locationId,
|
||||
'description': description,
|
||||
'victim_count': victimCount,
|
||||
'status': status?.toString().split('.').last ?? 'open',
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the CrimeIncidentModel with updated fields
|
||||
CrimeIncidentModel copyWith({
|
||||
String? id,
|
||||
String? crimeId,
|
||||
String? crimeCategoryId,
|
||||
String? locationId,
|
||||
String? description,
|
||||
int? victimCount,
|
||||
CrimeStatus? status,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
DateTime? timestamp,
|
||||
}) {
|
||||
return CrimeIncidentModel(
|
||||
id: id ?? this.id,
|
||||
crimeId: crimeId ?? this.crimeId,
|
||||
crimeCategoryId: crimeCategoryId ?? this.crimeCategoryId,
|
||||
locationId: locationId ?? this.locationId,
|
||||
description: description ?? this.description,
|
||||
victimCount: victimCount ?? this.victimCount,
|
||||
status: status ?? this.status,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
);
|
||||
}
|
||||
|
||||
static CrimeStatus? _parseStatus(String? status) {
|
||||
if (status == null) return CrimeStatus.open;
|
||||
|
||||
switch (status) {
|
||||
case 'open':
|
||||
return CrimeStatus.open;
|
||||
case 'closed':
|
||||
return CrimeStatus.closed;
|
||||
case 'under_investigation':
|
||||
return CrimeStatus.underInvestigation;
|
||||
case 'resolved':
|
||||
return CrimeStatus.resolved;
|
||||
case 'unresolved':
|
||||
return CrimeStatus.unresolved;
|
||||
default:
|
||||
return CrimeStatus.open;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CrimeIncidentModel(id: $id, crimeId: $crimeId, status: ${status.toString()})';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
import 'package:sigap/src/features/map/models/districts_model.dart';
|
||||
import 'package:sigap/src/features/panic-button/models/crime_incidents_model.dart';
|
||||
|
||||
enum CrimeRates { low, medium, high, critical }
|
||||
|
||||
class CrimeModel {
|
||||
final String id;
|
||||
final String districtId;
|
||||
final DateTime? createdAt;
|
||||
final CrimeRates level;
|
||||
final String? method;
|
||||
final int? month;
|
||||
final int numberOfCrime;
|
||||
final double score;
|
||||
final DateTime? updatedAt;
|
||||
final int? year;
|
||||
final String? sourceType;
|
||||
final int crimeCleared;
|
||||
final double avgCrime;
|
||||
final List<CrimeIncidentModel>? crimeIncidents;
|
||||
final DistrictModel? district;
|
||||
|
||||
CrimeModel({
|
||||
required this.id,
|
||||
required this.districtId,
|
||||
this.createdAt,
|
||||
required this.level,
|
||||
this.method,
|
||||
this.month,
|
||||
required this.numberOfCrime,
|
||||
required this.score,
|
||||
this.updatedAt,
|
||||
this.year,
|
||||
this.sourceType,
|
||||
required this.crimeCleared,
|
||||
required this.avgCrime,
|
||||
this.crimeIncidents,
|
||||
this.district,
|
||||
});
|
||||
|
||||
// Create a CrimeModel instance from a JSON object
|
||||
factory CrimeModel.fromJson(Map<String, dynamic> json) {
|
||||
return CrimeModel(
|
||||
id: json['id'] as String,
|
||||
districtId: json['district_id'] as String,
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
level: _parseCrimeRate(json['level'] as String),
|
||||
method: json['method'] as String?,
|
||||
month: json['month'] as int?,
|
||||
numberOfCrime: json['number_of_crime'] as int,
|
||||
score: json['score'].toDouble(),
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
year: json['year'] as int?,
|
||||
sourceType: json['source_type'] as String?,
|
||||
crimeCleared: json['crime_cleared'] as int,
|
||||
avgCrime: json['avg_crime'].toDouble(),
|
||||
crimeIncidents:
|
||||
json['crime_incidents'] != null
|
||||
? (json['crime_incidents'] as List)
|
||||
.map(
|
||||
(e) =>
|
||||
CrimeIncidentModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
district:
|
||||
json['districts'] != null
|
||||
? DistrictModel.fromJson(
|
||||
json['districts'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a CrimeModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'district_id': districtId,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'level': level.name,
|
||||
'method': method,
|
||||
'month': month,
|
||||
'number_of_crime': numberOfCrime,
|
||||
'score': score,
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
'year': year,
|
||||
'source_type': sourceType,
|
||||
'crime_cleared': crimeCleared,
|
||||
'avg_crime': avgCrime,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the CrimeModel with updated fields
|
||||
CrimeModel copyWith({
|
||||
String? id,
|
||||
String? districtId,
|
||||
DateTime? createdAt,
|
||||
CrimeRates? level,
|
||||
String? method,
|
||||
int? month,
|
||||
int? numberOfCrime,
|
||||
double? score,
|
||||
DateTime? updatedAt,
|
||||
int? year,
|
||||
String? sourceType,
|
||||
int? crimeCleared,
|
||||
double? avgCrime,
|
||||
List<CrimeIncidentModel>? crimeIncidents,
|
||||
DistrictModel? district,
|
||||
}) {
|
||||
return CrimeModel(
|
||||
id: id ?? this.id,
|
||||
districtId: districtId ?? this.districtId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
level: level ?? this.level,
|
||||
method: method ?? this.method,
|
||||
month: month ?? this.month,
|
||||
numberOfCrime: numberOfCrime ?? this.numberOfCrime,
|
||||
score: score ?? this.score,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
year: year ?? this.year,
|
||||
sourceType: sourceType ?? this.sourceType,
|
||||
crimeCleared: crimeCleared ?? this.crimeCleared,
|
||||
avgCrime: avgCrime ?? this.avgCrime,
|
||||
crimeIncidents: crimeIncidents ?? this.crimeIncidents,
|
||||
district: district ?? this.district,
|
||||
);
|
||||
}
|
||||
|
||||
static CrimeRates _parseCrimeRate(String rate) {
|
||||
switch (rate.toLowerCase()) {
|
||||
case 'low':
|
||||
return CrimeRates.low;
|
||||
case 'medium':
|
||||
return CrimeRates.medium;
|
||||
case 'high':
|
||||
return CrimeRates.high;
|
||||
case 'critical':
|
||||
return CrimeRates.critical;
|
||||
default:
|
||||
return CrimeRates.low;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'CrimeModel(id: $id, districtId: $districtId, level: ${level.name}, year: $year, month: $month)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import 'package:sigap/src/features/map/models/locations_model.dart';
|
||||
|
||||
class EventModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? description;
|
||||
final String code;
|
||||
final DateTime createdAt;
|
||||
final String userId;
|
||||
final List<LocationModel>? locations;
|
||||
|
||||
EventModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.description,
|
||||
required this.code,
|
||||
required this.createdAt,
|
||||
required this.userId,
|
||||
this.locations,
|
||||
});
|
||||
|
||||
// Create an EventModel instance from a JSON object
|
||||
factory EventModel.fromJson(Map<String, dynamic> json) {
|
||||
return EventModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String?,
|
||||
code: json['code'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
userId: json['user_id'] as String,
|
||||
locations:
|
||||
json['locations'] != null
|
||||
? (json['locations'] as List)
|
||||
.map((e) => LocationModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert an EventModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'description': description,
|
||||
'code': code,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'user_id': userId,
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the EventModel with updated fields
|
||||
EventModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? description,
|
||||
String? code,
|
||||
DateTime? createdAt,
|
||||
String? userId,
|
||||
List<LocationModel>? locations,
|
||||
}) {
|
||||
return EventModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
description: description ?? this.description,
|
||||
code: code ?? this.code,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
userId: userId ?? this.userId,
|
||||
locations: locations ?? this.locations,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EventModel(id: $id, name: $name, code: $code)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
import 'package:sigap/src/features/panic-button/models/incident_logs_model.dart';
|
||||
|
||||
class EvidenceModel {
|
||||
final String id;
|
||||
final String incidentId;
|
||||
final String type;
|
||||
final String url;
|
||||
final String? description;
|
||||
final String? caption;
|
||||
final Map<String, dynamic>? metadata;
|
||||
final DateTime? uploadedAt;
|
||||
final IncidentLogModel? incident;
|
||||
|
||||
EvidenceModel({
|
||||
required this.id,
|
||||
required this.incidentId,
|
||||
required this.type,
|
||||
required this.url,
|
||||
this.description,
|
||||
this.caption,
|
||||
this.metadata,
|
||||
this.uploadedAt,
|
||||
this.incident,
|
||||
});
|
||||
|
||||
// Create an EvidenceModel instance from a JSON object
|
||||
factory EvidenceModel.fromJson(Map<String, dynamic> json) {
|
||||
return EvidenceModel(
|
||||
id: json['id'] as String,
|
||||
incidentId: json['incident_id'] as String,
|
||||
type: json['type'] as String,
|
||||
url: json['url'] as String,
|
||||
description: json['description'] as String?,
|
||||
caption: json['caption'] as String?,
|
||||
metadata: json['metadata'] as Map<String, dynamic>?,
|
||||
uploadedAt:
|
||||
json['uploaded_at'] != null
|
||||
? DateTime.parse(json['uploaded_at'] as String)
|
||||
: null,
|
||||
incident:
|
||||
json['incident'] != null
|
||||
? IncidentLogModel.fromJson(
|
||||
json['incident'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert an EvidenceModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'incident_id': incidentId,
|
||||
'type': type,
|
||||
'url': url,
|
||||
'description': description,
|
||||
'caption': caption,
|
||||
'metadata': metadata,
|
||||
'uploaded_at': uploadedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the EvidenceModel with updated fields
|
||||
EvidenceModel copyWith({
|
||||
String? id,
|
||||
String? incidentId,
|
||||
String? type,
|
||||
String? url,
|
||||
String? description,
|
||||
String? caption,
|
||||
Map<String, dynamic>? metadata,
|
||||
DateTime? uploadedAt,
|
||||
IncidentLogModel? incident,
|
||||
}) {
|
||||
return EvidenceModel(
|
||||
id: id ?? this.id,
|
||||
incidentId: incidentId ?? this.incidentId,
|
||||
type: type ?? this.type,
|
||||
url: url ?? this.url,
|
||||
description: description ?? this.description,
|
||||
caption: caption ?? this.caption,
|
||||
metadata: metadata ?? this.metadata,
|
||||
uploadedAt: uploadedAt ?? this.uploadedAt,
|
||||
incident: incident ?? this.incident,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'EvidenceModel(id: $id, type: $type)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import 'package:sigap/src/features/panic-button/models/evidences_model.dart';
|
||||
|
||||
class IncidentLogModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String locationId;
|
||||
final String categoryId;
|
||||
final String? description;
|
||||
final String? source;
|
||||
final DateTime time;
|
||||
final bool? verified;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final List<EvidenceModel>? evidence;
|
||||
|
||||
IncidentLogModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.locationId,
|
||||
required this.categoryId,
|
||||
this.description,
|
||||
this.source = 'manual',
|
||||
required this.time,
|
||||
this.verified = false,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.evidence,
|
||||
});
|
||||
|
||||
// Create an IncidentLogModel instance from a JSON object
|
||||
factory IncidentLogModel.fromJson(Map<String, dynamic> json) {
|
||||
return IncidentLogModel(
|
||||
id: json['id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
locationId: json['location_id'] as String,
|
||||
categoryId: json['category_id'] as String,
|
||||
description: json['description'] as String?,
|
||||
source: json['source'] as String? ?? 'manual',
|
||||
time: DateTime.parse(json['time'] as String),
|
||||
verified: json['verified'] as bool? ?? false,
|
||||
createdAt:
|
||||
json['created_at'] != null
|
||||
? DateTime.parse(json['created_at'])
|
||||
: null,
|
||||
updatedAt:
|
||||
json['updated_at'] != null
|
||||
? DateTime.parse(json['updated_at'])
|
||||
: null,
|
||||
evidence:
|
||||
json['evidence'] != null
|
||||
? (json['evidence'] as List)
|
||||
.map((e) => EvidenceModel.fromJson(e as Map<String, dynamic>))
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert an IncidentLogModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'user_id': userId,
|
||||
'location_id': locationId,
|
||||
'category_id': categoryId,
|
||||
'description': description,
|
||||
'source': source,
|
||||
'time': time.toIso8601String(),
|
||||
'verified': verified,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the IncidentLogModel with updated fields
|
||||
IncidentLogModel copyWith({
|
||||
String? id,
|
||||
String? userId,
|
||||
String? locationId,
|
||||
String? categoryId,
|
||||
String? description,
|
||||
String? source,
|
||||
DateTime? time,
|
||||
bool? verified,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<EvidenceModel>? evidence,
|
||||
}) {
|
||||
return IncidentLogModel(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
locationId: locationId ?? this.locationId,
|
||||
categoryId: categoryId ?? this.categoryId,
|
||||
description: description ?? this.description,
|
||||
source: source ?? this.source,
|
||||
time: time ?? this.time,
|
||||
verified: verified ?? this.verified,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
evidence: evidence ?? this.evidence,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'IncidentLogModel(id: $id, locationId: $locationId, time: $time)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export 'crime_incidents_model.dart';
|
||||
export 'crimes_model.dart';
|
||||
export 'events_model.dart';
|
||||
export 'evidences_model.dart';
|
||||
export 'incident_logs_model.dart';
|
||||
export 'sessions_model.dart';
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:sigap/src/features/panic-button/models/events_model.dart';
|
||||
|
||||
enum SessionStatus { active, inactive }
|
||||
|
||||
class SessionsModel {
|
||||
final String id;
|
||||
final String userId;
|
||||
final String eventId;
|
||||
final SessionStatus status;
|
||||
final DateTime createdAt;
|
||||
final EventModel event;
|
||||
|
||||
SessionsModel({
|
||||
required this.id,
|
||||
required this.userId,
|
||||
required this.eventId,
|
||||
required this.status,
|
||||
required this.createdAt,
|
||||
required this.event,
|
||||
});
|
||||
|
||||
// Create a SessionsModel instance from a JSON object
|
||||
factory SessionsModel.fromJson(Map<String, dynamic> json) {
|
||||
return SessionsModel(
|
||||
id: json['id'] as String,
|
||||
userId: json['user_id'] as String,
|
||||
eventId: json['event_id'] as String,
|
||||
status: SessionStatus.values.firstWhere(
|
||||
(e) => e.toString() == 'SessionStatus.${json['status']}',
|
||||
),
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
event: EventModel.fromJson(json['event'] as Map<String, dynamic>),
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a SessionsModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'user_id': userId,
|
||||
'event_id': eventId,
|
||||
'status': status.toString().split('.').last,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'event': event.toJson(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the SessionsModel with updated fields
|
||||
SessionsModel copyWith({
|
||||
String? id,
|
||||
String? userId,
|
||||
String? eventId,
|
||||
SessionStatus? status,
|
||||
DateTime? createdAt,
|
||||
EventModel? event,
|
||||
}) {
|
||||
return SessionsModel(
|
||||
id: id ?? this.id,
|
||||
userId: userId ?? this.userId,
|
||||
eventId: eventId ?? this.eventId,
|
||||
status: status ?? this.status,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
event: event ?? this.event,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SessionsModel(id: $id, userId: $userId, eventId: $eventId, status: $status)';
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
export 'users_model.dart';
|
||||
export 'officers_model.dart';
|
||||
export 'permissions_model.dart';
|
||||
export 'profile_model.dart';
|
||||
export 'resources_model.dart';
|
||||
export 'roles_model.dart';
|
||||
export 'users_model.dart';
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
import 'package:sigap/src/features/personalization/models/resources_model.dart';
|
||||
import 'package:sigap/src/features/personalization/models/roles_model.dart';
|
||||
|
||||
class PermissionModel {
|
||||
final String id;
|
||||
final String action;
|
||||
final String resourceId;
|
||||
final String roleId;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final ResourceModel? resource;
|
||||
final RoleModel? role;
|
||||
|
||||
PermissionModel({
|
||||
required this.id,
|
||||
required this.action,
|
||||
required this.resourceId,
|
||||
required this.roleId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.resource,
|
||||
this.role,
|
||||
});
|
||||
|
||||
// Create a PermissionModel instance from a JSON object
|
||||
factory PermissionModel.fromJson(Map<String, dynamic> json) {
|
||||
return PermissionModel(
|
||||
id: json['id'] as String,
|
||||
action: json['action'] as String,
|
||||
resourceId: json['resource_id'] as String,
|
||||
roleId: json['role_id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
resource:
|
||||
json['resource'] != null
|
||||
? ResourceModel.fromJson(json['resource'] as Map<String, dynamic>)
|
||||
: null,
|
||||
role:
|
||||
json['role'] != null
|
||||
? RoleModel.fromJson(json['role'] as Map<String, dynamic>)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a PermissionModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'action': action,
|
||||
'resource_id': resourceId,
|
||||
'role_id': roleId,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the PermissionModel with updated fields
|
||||
PermissionModel copyWith({
|
||||
String? id,
|
||||
String? action,
|
||||
String? resourceId,
|
||||
String? roleId,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
ResourceModel? resource,
|
||||
RoleModel? role,
|
||||
}) {
|
||||
return PermissionModel(
|
||||
id: id ?? this.id,
|
||||
action: action ?? this.action,
|
||||
resourceId: resourceId ?? this.resourceId,
|
||||
roleId: roleId ?? this.roleId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
resource: resource ?? this.resource,
|
||||
role: role ?? this.role,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PermissionModel(id: $id, action: $action)';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import 'package:sigap/src/features/personalization/models/permissions_model.dart';
|
||||
|
||||
class ResourceModel {
|
||||
final String id;
|
||||
final String name;
|
||||
final String? type;
|
||||
final String? description;
|
||||
final String? instanceRole;
|
||||
final String? relations;
|
||||
final Map<String, dynamic>? attributes;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final List<PermissionModel>? permissions;
|
||||
|
||||
ResourceModel({
|
||||
required this.id,
|
||||
required this.name,
|
||||
this.type,
|
||||
this.description,
|
||||
this.instanceRole,
|
||||
this.relations,
|
||||
this.attributes,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
this.permissions,
|
||||
});
|
||||
|
||||
// Create a ResourceModel instance from a JSON object
|
||||
factory ResourceModel.fromJson(Map<String, dynamic> json) {
|
||||
return ResourceModel(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
type: json['type'] as String?,
|
||||
description: json['description'] as String?,
|
||||
instanceRole: json['instance_role'] as String?,
|
||||
relations: json['relations'] as String?,
|
||||
attributes: json['attributes'] as Map<String, dynamic>?,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
permissions:
|
||||
json['permissions'] != null
|
||||
? (json['permissions'] as List)
|
||||
.map(
|
||||
(e) => PermissionModel.fromJson(e as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
// Convert a ResourceModel instance to a JSON object
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'type': type,
|
||||
'description': description,
|
||||
'instance_role': instanceRole,
|
||||
'relations': relations,
|
||||
'attributes': attributes,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
'updated_at': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create a copy of the ResourceModel with updated fields
|
||||
ResourceModel copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? type,
|
||||
String? description,
|
||||
String? instanceRole,
|
||||
String? relations,
|
||||
Map<String, dynamic>? attributes,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
List<PermissionModel>? permissions,
|
||||
}) {
|
||||
return ResourceModel(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
description: description ?? this.description,
|
||||
instanceRole: instanceRole ?? this.instanceRole,
|
||||
relations: relations ?? this.relations,
|
||||
attributes: attributes ?? this.attributes,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
permissions: permissions ?? this.permissions,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ResourceModel(id: $id, name: $name)';
|
||||
}
|
||||
}
|
|
@ -10,6 +10,7 @@ class CustomTextField extends StatelessWidget {
|
|||
final TextInputType keyboardType;
|
||||
final String? errorText;
|
||||
final bool autofocus;
|
||||
final int maxLines;
|
||||
final TextInputAction textInputAction;
|
||||
final Function(String)? onChanged;
|
||||
|
||||
|
@ -23,6 +24,7 @@ class CustomTextField extends StatelessWidget {
|
|||
this.keyboardType = TextInputType.text,
|
||||
this.errorText,
|
||||
this.autofocus = false,
|
||||
this.maxLines = 1,
|
||||
this.textInputAction = TextInputAction.next,
|
||||
this.onChanged,
|
||||
});
|
||||
|
@ -48,6 +50,7 @@ class CustomTextField extends StatelessWidget {
|
|||
keyboardType: keyboardType,
|
||||
autofocus: autofocus,
|
||||
textInputAction: textInputAction,
|
||||
maxLines: maxLines,
|
||||
onChanged: onChanged,
|
||||
style: TextStyle(color: TColors.textPrimary, fontSize: 16),
|
||||
decoration: InputDecoration(
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
|
@ -14,6 +15,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
|
||||
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
gtk
|
||||
url_launcher_linux
|
||||
)
|
||||
|
|
|
@ -10,8 +10,10 @@ import connectivity_plus
|
|||
import file_picker
|
||||
import file_selector_macos
|
||||
import flutter_local_notifications
|
||||
import flutter_secure_storage_macos
|
||||
import geolocator_apple
|
||||
import google_sign_in_ios
|
||||
import local_auth_darwin
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
@ -22,8 +24,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
|
|
@ -427,6 +427,54 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
flutter_secure_storage:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_secure_storage
|
||||
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.2.4"
|
||||
flutter_secure_storage_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_linux
|
||||
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.3"
|
||||
flutter_secure_storage_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_macos
|
||||
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
flutter_secure_storage_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_platform_interface
|
||||
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
flutter_secure_storage_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_web
|
||||
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
flutter_secure_storage_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_secure_storage_windows
|
||||
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
@ -453,6 +501,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
geocoding:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: geocoding
|
||||
sha256: d580c801cba9386b4fac5047c4c785a4e19554f46be42f4f5e5b7deacd088a66
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
geocoding_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geocoding_android
|
||||
sha256: "1b13eca79b11c497c434678fed109c2be020b158cec7512c848c102bc7232603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.3.1"
|
||||
geocoding_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geocoding_ios
|
||||
sha256: "94ddba60387501bd1c11e18dca7c5a9e8c645d6e3da9c38b9762434941870c24"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
geocoding_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: geocoding_platform_interface
|
||||
sha256: "8c2c8226e5c276594c2e18bfe88b19110ed770aeb7c1ab50ede570be8b92229b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
geolocator:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -781,6 +861,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
local_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: local_auth
|
||||
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
local_auth_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_android
|
||||
sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.49"
|
||||
local_auth_darwin:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_darwin
|
||||
sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.3"
|
||||
local_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_platform_interface
|
||||
sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.10"
|
||||
local_auth_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: local_auth_windows
|
||||
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.11"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -68,6 +68,7 @@ dependencies:
|
|||
flutter_map:
|
||||
latlong2:
|
||||
geolocator:
|
||||
geocoding:
|
||||
|
||||
# Icons
|
||||
iconsax:
|
||||
|
@ -83,6 +84,8 @@ dependencies:
|
|||
|
||||
# Authentication
|
||||
google_sign_in:
|
||||
local_auth:
|
||||
flutter_secure_storage:
|
||||
|
||||
# API Services
|
||||
dio:
|
||||
|
|
|
@ -0,0 +1,511 @@
|
|||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["postgresqlExtensions"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
extensions = [pgcrypto, postgis, uuid_ossp(map: "uuid-ossp", schema: "extensions")]
|
||||
}
|
||||
|
||||
model profiles {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
user_id String @unique @db.Uuid
|
||||
nik String @unique @default("") @db.VarChar(100)
|
||||
avatar String? @db.VarChar(355)
|
||||
username String? @unique @db.VarChar(255)
|
||||
first_name String? @db.VarChar(255)
|
||||
last_name String? @db.VarChar(255)
|
||||
bio String? @db.VarChar
|
||||
address Json? @db.Json
|
||||
birth_date DateTime?
|
||||
users users @relation(fields: [user_id], references: [id])
|
||||
|
||||
@@index([nik], map: "idx_profiles_nik")
|
||||
@@index([user_id])
|
||||
@@index([username])
|
||||
}
|
||||
|
||||
model users {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
roles_id String @db.Uuid
|
||||
email String @unique @db.VarChar(255)
|
||||
phone String? @unique @db.VarChar(20)
|
||||
encrypted_password String? @db.VarChar(255)
|
||||
invited_at DateTime? @db.Timestamptz(6)
|
||||
confirmed_at DateTime? @db.Timestamptz(6)
|
||||
email_confirmed_at DateTime? @db.Timestamptz(6)
|
||||
recovery_sent_at DateTime? @db.Timestamptz(6)
|
||||
last_sign_in_at DateTime? @db.Timestamptz(6)
|
||||
app_metadata Json?
|
||||
user_metadata Json?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
is_banned Boolean @default(false)
|
||||
spoofing_attempts Int @default(0)
|
||||
panic_strike Int @default(0)
|
||||
banned_reason String? @db.VarChar(255)
|
||||
banned_until DateTime? @db.Timestamptz(6)
|
||||
is_anonymous Boolean @default(false)
|
||||
events events[]
|
||||
incident_logs incident_logs[]
|
||||
location_logs location_logs[]
|
||||
profile profiles?
|
||||
sessions sessions[]
|
||||
role roles @relation(fields: [roles_id], references: [id])
|
||||
panic_button_logs panic_button_logs[]
|
||||
|
||||
@@index([is_anonymous])
|
||||
@@index([created_at])
|
||||
@@index([updated_at])
|
||||
}
|
||||
|
||||
model roles {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String @unique @db.VarChar(255)
|
||||
description String?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
permissions permissions[]
|
||||
users users[]
|
||||
officers officers[]
|
||||
}
|
||||
|
||||
model sessions {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
user_id String @db.Uuid
|
||||
event_id String @db.Uuid
|
||||
status session_status @default(active)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
event events @relation(fields: [event_id], references: [id])
|
||||
user users @relation(fields: [user_id], references: [id])
|
||||
|
||||
@@index([user_id], map: "idx_sessions_user_id")
|
||||
@@index([event_id], map: "idx_sessions_event_id")
|
||||
@@index([status], map: "idx_sessions_status")
|
||||
}
|
||||
|
||||
model events {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
code String @unique @default(nanoid(10))
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
user_id String @db.Uuid
|
||||
users users @relation(fields: [user_id], references: [id])
|
||||
locations locations[]
|
||||
sessions sessions[]
|
||||
|
||||
@@index([name], map: "idx_events_name")
|
||||
@@index([code], map: "idx_events_code")
|
||||
@@index([id], map: "idx_events_id")
|
||||
}
|
||||
|
||||
model resources {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String @unique @db.VarChar(255)
|
||||
type String?
|
||||
description String?
|
||||
instance_role String?
|
||||
relations String?
|
||||
attributes Json?
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
permissions permissions[]
|
||||
}
|
||||
|
||||
model permissions {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
action String
|
||||
resource_id String @db.Uuid
|
||||
role_id String @db.Uuid
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @updatedAt @db.Timestamptz(6)
|
||||
resource resources @relation(fields: [resource_id], references: [id])
|
||||
role roles @relation(fields: [role_id], references: [id])
|
||||
}
|
||||
|
||||
model cities {
|
||||
id String @id @db.VarChar(20)
|
||||
name String @db.VarChar(100)
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
districts districts[]
|
||||
units units[]
|
||||
|
||||
@@index([name], map: "idx_cities_name")
|
||||
}
|
||||
|
||||
model crime_incidents {
|
||||
id String @id @db.VarChar(20)
|
||||
crime_id String @db.VarChar(20)
|
||||
crime_category_id String @db.VarChar(20)
|
||||
location_id String @db.Uuid
|
||||
description String
|
||||
victim_count Int
|
||||
status crime_status? @default(open)
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
timestamp DateTime @db.Timestamptz(6)
|
||||
crime_categories crime_categories @relation(fields: [crime_category_id], references: [id], onUpdate: NoAction)
|
||||
crimes crimes @relation(fields: [crime_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
|
||||
locations locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([crime_category_id], map: "idx_crime_incidents_crime_category_id")
|
||||
@@index([timestamp], map: "idx_crime_incidents_date")
|
||||
@@index([location_id], map: "idx_crime_incidents_location_id")
|
||||
@@index([crime_id], map: "idx_crime_incidents_crime_id")
|
||||
@@index([status], map: "idx_crime_incidents_status")
|
||||
}
|
||||
|
||||
model crime_categories {
|
||||
id String @id @db.VarChar(20)
|
||||
name String @db.VarChar(255)
|
||||
description String
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
type String? @db.VarChar(100)
|
||||
crime_incidents crime_incidents[]
|
||||
incident_logs incident_logs[]
|
||||
|
||||
@@index([name], map: "idx_crime_categories_name")
|
||||
}
|
||||
|
||||
model crimes {
|
||||
id String @id @db.VarChar(20)
|
||||
district_id String @db.VarChar(20)
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
level crime_rates @default(low)
|
||||
method String? @db.VarChar(100)
|
||||
month Int?
|
||||
number_of_crime Int @default(0)
|
||||
score Float @default(0)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
year Int?
|
||||
source_type String? @db.VarChar(100)
|
||||
crime_cleared Int @default(0)
|
||||
avg_crime Float @default(0)
|
||||
crime_incidents crime_incidents[]
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([district_id, year, month], map: "idx_crimes_district_id_year_month")
|
||||
@@index([month, year], map: "idx_crimes_month_year")
|
||||
@@index([month], map: "idx_crimes_month")
|
||||
@@index([year], map: "idx_crimes_year")
|
||||
@@index([district_id, month], map: "idx_crimes_district_id_month")
|
||||
}
|
||||
|
||||
model demographics {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
district_id String @db.VarChar(20)
|
||||
population Int
|
||||
number_of_unemployed Int
|
||||
population_density Float
|
||||
year Int
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@unique([district_id, year])
|
||||
@@index([year], map: "idx_demographics_year")
|
||||
}
|
||||
|
||||
model districts {
|
||||
id String @id @db.VarChar(20)
|
||||
city_id String @db.VarChar(20)
|
||||
name String @db.VarChar(100)
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
crimes crimes[]
|
||||
demographics demographics[]
|
||||
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
geographics geographics[]
|
||||
locations locations[]
|
||||
units units?
|
||||
|
||||
@@index([city_id], map: "idx_districts_city_id")
|
||||
@@index([name], map: "idx_districts_name")
|
||||
}
|
||||
|
||||
model locations {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
district_id String @db.VarChar(20)
|
||||
event_id String @db.Uuid
|
||||
address String? @db.VarChar(255)
|
||||
type String? @db.VarChar(100)
|
||||
latitude Float
|
||||
longitude Float
|
||||
land_area Float?
|
||||
polygon Unsupported("geometry")?
|
||||
geometry Unsupported("geometry")?
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
location Unsupported("geography")
|
||||
distance_to_unit Float?
|
||||
crime_incidents crime_incidents[]
|
||||
incident_logs incident_logs[]
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
event events @relation(fields: [event_id], references: [id])
|
||||
patrol_units patrol_units[]
|
||||
|
||||
@@index([district_id], map: "idx_locations_district_id")
|
||||
@@index([type], map: "idx_locations_type")
|
||||
@@index([location], map: "idx_locations_geography", type: Gist)
|
||||
@@index([location], map: "idx_locations_location_gist", type: Gist)
|
||||
}
|
||||
|
||||
model incident_logs {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
user_id String @db.Uuid
|
||||
location_id String @db.Uuid
|
||||
category_id String @db.VarChar(20)
|
||||
description String?
|
||||
source String? @default("manual")
|
||||
time DateTime @db.Timestamptz(6)
|
||||
verified Boolean? @default(false)
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
crime_categories crime_categories @relation(fields: [category_id], references: [id], map: "fk_incident_category")
|
||||
locations locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
user users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
evidence evidence[]
|
||||
panic_button_logs panic_button_logs[]
|
||||
|
||||
@@index([category_id], map: "idx_incident_logs_category_id")
|
||||
@@index([time], map: "idx_incident_logs_time")
|
||||
}
|
||||
|
||||
model evidence {
|
||||
id String @id @unique @db.VarChar(20)
|
||||
incident_id String @db.Uuid
|
||||
type String @db.VarChar(50) // contoh: photo, video, document, images
|
||||
url String @db.Text
|
||||
description String? @db.VarChar(255)
|
||||
caption String? @db.VarChar(255)
|
||||
metadata Json?
|
||||
uploaded_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
|
||||
incident incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([incident_id], map: "idx_evidence_incident_id")
|
||||
}
|
||||
|
||||
model units {
|
||||
code_unit String @id @unique @db.VarChar(20)
|
||||
district_id String? @unique @db.VarChar(20)
|
||||
name String @db.VarChar(100)
|
||||
description String?
|
||||
type unit_type
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
address String?
|
||||
land_area Float?
|
||||
latitude Float
|
||||
longitude Float
|
||||
location Unsupported("geography")
|
||||
city_id String @db.VarChar(20)
|
||||
phone String?
|
||||
unit_statistics unit_statistics[]
|
||||
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
districts districts? @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
officers officers[]
|
||||
patrol_units patrol_units[]
|
||||
|
||||
@@index([name], map: "idx_units_name")
|
||||
@@index([type], map: "idx_units_type")
|
||||
@@index([code_unit], map: "idx_units_code_unit")
|
||||
@@index([district_id], map: "idx_units_district_id")
|
||||
@@index([location], map: "idx_unit_location", type: Gist)
|
||||
@@index([district_id, location], map: "idx_units_location_district")
|
||||
@@index([location], map: "idx_units_location_gist", type: Gist)
|
||||
@@index([location], type: Gist)
|
||||
@@index([location], map: "units_location_idx1", type: Gist)
|
||||
@@index([location], map: "units_location_idx2", type: Gist)
|
||||
}
|
||||
|
||||
model patrol_units {
|
||||
id String @id @unique @db.VarChar(100)
|
||||
unit_id String @db.VarChar(20)
|
||||
location_id String @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
type String @db.VarChar(50)
|
||||
status String @db.VarChar(50)
|
||||
radius Float
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
|
||||
members officers[]
|
||||
location locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
unit units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([unit_id], map: "idx_patrol_units_unit_id")
|
||||
@@index([location_id], map: "idx_patrol_units_location_id")
|
||||
@@index([name], map: "idx_patrol_units_name")
|
||||
@@index([type], map: "idx_patrol_units_type")
|
||||
@@index([status], map: "idx_patrol_units_status")
|
||||
}
|
||||
|
||||
model officers {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
unit_id String @db.VarChar(20)
|
||||
role_id String @db.Uuid
|
||||
patrol_unit_id String @db.VarChar(100)
|
||||
nrp String @unique @db.VarChar(100)
|
||||
name String @db.VarChar(100)
|
||||
rank String? @db.VarChar(100)
|
||||
position String? @db.VarChar(100)
|
||||
phone String? @db.VarChar(100)
|
||||
email String? @db.VarChar(255)
|
||||
avatar String?
|
||||
valid_until DateTime?
|
||||
qr_code String?
|
||||
is_banned Boolean @default(false)
|
||||
panic_strike Int @default(0)
|
||||
spoofing_attempts Int @default(0)
|
||||
banned_reason String? @db.VarChar(255)
|
||||
banned_until DateTime?
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
units units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||
roles roles @relation(fields: [role_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
patrol_units patrol_units? @relation(fields: [patrol_unit_id], references: [id])
|
||||
panic_button_logs panic_button_logs[]
|
||||
|
||||
@@index([unit_id], map: "idx_officers_unit_id")
|
||||
@@index([nrp], map: "idx_officers_nrp")
|
||||
@@index([name], map: "idx_officers_name")
|
||||
@@index([rank], map: "idx_officers_rank")
|
||||
@@index([position], map: "idx_officers_position")
|
||||
}
|
||||
|
||||
model unit_statistics {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
crime_total Int
|
||||
crime_cleared Int
|
||||
percentage Float?
|
||||
pending Int?
|
||||
month Int
|
||||
year Int
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
code_unit String @db.VarChar(20)
|
||||
units units @relation(fields: [code_unit], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@unique([code_unit, month, year])
|
||||
@@index([year, month], map: "idx_unit_statistics_year_month")
|
||||
}
|
||||
|
||||
model geographics {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
district_id String @db.VarChar(20)
|
||||
address String?
|
||||
longitude Float
|
||||
latitude Float
|
||||
land_area Float?
|
||||
polygon Unsupported("geometry")?
|
||||
geometry Unsupported("geometry")?
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
description String?
|
||||
type String? @db.VarChar(100)
|
||||
location Unsupported("geography")
|
||||
year Int?
|
||||
districts districts @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([district_id], map: "idx_geographics_district_id")
|
||||
@@index([type], map: "idx_geographics_type")
|
||||
@@index([district_id, year], map: "idx_geographics_district_id_year")
|
||||
@@index([location], map: "idx_geographics_location", type: Gist)
|
||||
}
|
||||
|
||||
model contact_messages {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
name String? @db.VarChar(255)
|
||||
email String? @db.VarChar(255)
|
||||
phone String? @db.VarChar(20)
|
||||
message_type String? @db.VarChar(50)
|
||||
message_type_label String? @db.VarChar(50)
|
||||
message String?
|
||||
status status_contact_messages @default(new)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @db.Timestamptz(6)
|
||||
}
|
||||
|
||||
model logs {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
action String @db.VarChar(100)
|
||||
entity String @db.VarChar(100)
|
||||
entity_id String? @db.VarChar(100)
|
||||
changes Json?
|
||||
user_id String? @db.VarChar(100)
|
||||
ip_address String? @db.VarChar(100)
|
||||
user_agent String? @db.VarChar(255)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
|
||||
@@index([entity])
|
||||
@@index([user_id])
|
||||
}
|
||||
|
||||
model panic_button_logs {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
user_id String @db.Uuid
|
||||
officer_id String? @db.Uuid
|
||||
incident_id String @db.Uuid
|
||||
timestamp DateTime @db.Timestamptz(6)
|
||||
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
officers officers? @relation(fields: [officer_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
incidents incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([user_id], map: "idx_panic_buttons_user_id")
|
||||
}
|
||||
|
||||
model location_logs {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
user_id String @db.Uuid
|
||||
latitude Float
|
||||
longitude Float
|
||||
location Unsupported("geography")
|
||||
timestamp DateTime @db.Timestamptz(6)
|
||||
description String? @db.VarChar(255)
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
|
||||
@@index([timestamp], map: "idx_location_logs_timestamp")
|
||||
@@index([user_id], map: "idx_location_logs_user_id")
|
||||
}
|
||||
|
||||
enum session_status {
|
||||
active
|
||||
completed
|
||||
}
|
||||
|
||||
enum status_contact_messages {
|
||||
new
|
||||
read
|
||||
replied
|
||||
closed
|
||||
}
|
||||
|
||||
enum crime_rates {
|
||||
low
|
||||
medium
|
||||
high
|
||||
critical
|
||||
}
|
||||
|
||||
enum crime_status {
|
||||
open
|
||||
closed
|
||||
under_investigation
|
||||
resolved
|
||||
unresolved
|
||||
}
|
||||
|
||||
enum unit_type {
|
||||
polda
|
||||
polsek
|
||||
polres
|
||||
other
|
||||
}
|
|
@ -9,7 +9,9 @@
|
|||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||
#include <geolocator_windows/geolocator_windows.h>
|
||||
#include <local_auth_windows/local_auth_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
|
@ -20,8 +22,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
GeolocatorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||
LocalAuthPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
|
|
|
@ -6,7 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||
app_links
|
||||
connectivity_plus
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
geolocator_windows
|
||||
local_auth_windows
|
||||
permission_handler_windows
|
||||
url_launcher_windows
|
||||
)
|
||||
|
|
|
@ -99,3 +99,11 @@ CREATE INDEX IF NOT EXISTS idx_units_location_district ON units (district_id, lo
|
|||
-- Analisis tabel setelah membuat index
|
||||
ANALYZE units;
|
||||
ANALYZE locations;
|
||||
|
||||
create or replace function delete_user()
|
||||
returns void as
|
||||
$$
|
||||
begin
|
||||
delete from auth.users where id = (select auth.uid());
|
||||
end;
|
||||
$$ language plpgsql security definer set search_path = '';
|
Loading…
Reference in New Issue