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/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:sigap/app.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:sigap/src/cores/services/supabase_service.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
|
@ -32,8 +35,14 @@ Future<void> main() async {
|
||||||
storageOptions: const StorageClientOptions(retryAttempts: 10),
|
storageOptions: const StorageClientOptions(retryAttempts: 10),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add this to your dependencies initialization:
|
// Initialize services
|
||||||
await Get.putAsync(() => SupabaseService().init());
|
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());
|
runApp(const App());
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.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/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart';
|
import 'package:sigap/src/features/auth/screens/signin/signin_screen.dart';
|
||||||
import 'package:sigap/src/features/onboarding/screens/onboarding/onboarding_screen.dart';
|
import 'package:sigap/src/features/onboarding/screens/onboarding/onboarding_screen.dart';
|
||||||
|
@ -16,17 +18,37 @@ class AuthenticationRepository extends GetxController {
|
||||||
// Variable
|
// Variable
|
||||||
final storage = GetStorage();
|
final storage = GetStorage();
|
||||||
final _supabase = SupabaseService.instance.client;
|
final _supabase = SupabaseService.instance.client;
|
||||||
|
final _locationService = LocationService.instance;
|
||||||
|
final _biometricService = Get.put(BiometricService());
|
||||||
|
|
||||||
// Getters that use the Supabase service
|
// Getters that use the Supabase service
|
||||||
User? get authUser => SupabaseService.instance.currentUser;
|
User? get authUser => SupabaseService.instance.currentUser;
|
||||||
String? get currentUserId => SupabaseService.instance.currentUserId;
|
String? get currentUserId => SupabaseService.instance.currentUserId;
|
||||||
// get isSessionExpired => authUser?.isExpired;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
FlutterNativeSplash.remove();
|
FlutterNativeSplash.remove();
|
||||||
screenRedirect();
|
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 {
|
screenRedirect() async {
|
||||||
|
@ -38,6 +60,12 @@ class AuthenticationRepository extends GetxController {
|
||||||
storage.read('isFirstTime') != true
|
storage.read('isFirstTime') != true
|
||||||
? Get.offAll(() => const SignInScreen())
|
? Get.offAll(() => const SignInScreen())
|
||||||
: Get.offAll(() => const OnboardingScreen());
|
: 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,
|
required String password,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
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(
|
final response = await _supabase.auth.signInWithPassword(
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
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;
|
return response;
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
throw TExceptions(e.message);
|
throw TExceptions(e.message);
|
||||||
|
@ -173,10 +220,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
// Reauthenticate user
|
// Reauthenticate user
|
||||||
await _supabase.auth.signInWithPassword(
|
await _supabase.auth.reauthenticate();
|
||||||
email: email,
|
|
||||||
password: currentPassword,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update password
|
// Update password
|
||||||
await _supabase.auth.updateUser(UserAttributes(password: newPassword));
|
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(
|
Future<AuthResponse> signUpWithCredential(
|
||||||
String email,
|
String email,
|
||||||
String password, {
|
String password,
|
||||||
|
String identifier, // NIK for users or NRP for officers
|
||||||
|
{
|
||||||
Map<String, dynamic>? userMetadata,
|
Map<String, dynamic>? userMetadata,
|
||||||
bool isOfficer = false,
|
bool isOfficer = false,
|
||||||
Map<String, dynamic>? officerData,
|
Map<String, dynamic>? officerData,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
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
|
// Prepare the complete user metadata
|
||||||
final metadata = userMetadata ?? {};
|
final metadata = userMetadata ?? {};
|
||||||
|
|
||||||
|
@ -332,6 +428,16 @@ class AuthenticationRepository extends GetxController {
|
||||||
metadata['officer_data'] = officerData;
|
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(
|
final authResponse = await _supabase.auth.signUp(
|
||||||
email: email,
|
email: email,
|
||||||
password: password,
|
password: password,
|
||||||
|
@ -345,6 +451,14 @@ class AuthenticationRepository extends GetxController {
|
||||||
throw TExceptions('Failed to sign up. Please try again.');
|
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;
|
return authResponse;
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
throw TExceptions(e.message);
|
throw TExceptions(e.message);
|
||||||
|
@ -355,10 +469,32 @@ class AuthenticationRepository extends GetxController {
|
||||||
} on PostgrestException catch (error) {
|
} on PostgrestException catch (error) {
|
||||||
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
throw TExceptions.fromCode(error.code ?? 'unknown_error');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
if (e is TExceptions) {
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
throw TExceptions('Something went wrong. Please try again later.');
|
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 -----------------
|
// ----------------- Logout -----------------
|
||||||
// [Sign Out] - SIGN OUT
|
// [Sign Out] - SIGN OUT
|
||||||
Future<void> signOut() async {
|
Future<void> signOut() async {
|
||||||
|
@ -382,7 +518,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
Future<void> deleteAccount() async {
|
Future<void> deleteAccount() async {
|
||||||
try {
|
try {
|
||||||
final userId = _supabase.auth.currentUser!.id;
|
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) {
|
} on AuthException catch (e) {
|
||||||
throw TExceptions(e.message);
|
throw TExceptions(e.message);
|
||||||
} on FormatException catch (_) {
|
} 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:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/cores/services/supabase_service.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/features/personalization/models/index.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.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';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
class StepFormController extends GetxController {
|
class StepFormController extends GetxController {
|
||||||
|
@ -10,6 +12,7 @@ class StepFormController extends GetxController {
|
||||||
|
|
||||||
// Role information
|
// Role information
|
||||||
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
|
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
|
||||||
|
|
||||||
|
|
||||||
// Current step index
|
// Current step index
|
||||||
final RxInt currentStep = 0.obs;
|
final RxInt currentStep = 0.obs;
|
||||||
|
@ -23,7 +26,7 @@ class StepFormController extends GetxController {
|
||||||
final addressController = TextEditingController();
|
final addressController = TextEditingController();
|
||||||
|
|
||||||
// Viewer-specific fields
|
// Viewer-specific fields
|
||||||
final emergencyNameController = TextEditingController();
|
final nikController = TextEditingController();
|
||||||
final emergencyPhoneController = TextEditingController();
|
final emergencyPhoneController = TextEditingController();
|
||||||
final relationshipController = TextEditingController();
|
final relationshipController = TextEditingController();
|
||||||
|
|
||||||
|
@ -39,7 +42,7 @@ class StepFormController extends GetxController {
|
||||||
final RxString addressError = ''.obs;
|
final RxString addressError = ''.obs;
|
||||||
|
|
||||||
// Error states - Viewer
|
// Error states - Viewer
|
||||||
final RxString emergencyNameError = ''.obs;
|
final RxString nikError = ''.obs;
|
||||||
final RxString emergencyPhoneError = ''.obs;
|
final RxString emergencyPhoneError = ''.obs;
|
||||||
final RxString relationshipError = ''.obs;
|
final RxString relationshipError = ''.obs;
|
||||||
|
|
||||||
|
@ -53,8 +56,7 @@ class StepFormController extends GetxController {
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
|
|
||||||
// Available units for officer role
|
// Available units for officer role
|
||||||
final RxList<Map<String, dynamic>> availableUnits =
|
final RxList<UnitModel> availableUnits = <UnitModel>[].obs;
|
||||||
<Map<String, dynamic>>[].obs;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
|
@ -94,18 +96,11 @@ class StepFormController extends GetxController {
|
||||||
// Here we would fetch units from repository
|
// Here we would fetch units from repository
|
||||||
// For now we'll use dummy data
|
// For now we'll use dummy data
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
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) {
|
} catch (e) {
|
||||||
Get.snackbar(
|
TLoaders.errorSnackBar(
|
||||||
'Error',
|
title: 'Error',
|
||||||
'Failed to load units: ${e.toString()}',
|
message: 'Failed to fetch available units: ${e.toString()}',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.red,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
|
@ -119,7 +114,7 @@ class StepFormController extends GetxController {
|
||||||
phoneController.dispose();
|
phoneController.dispose();
|
||||||
addressController.dispose();
|
addressController.dispose();
|
||||||
|
|
||||||
emergencyNameController.dispose();
|
nikController.dispose();
|
||||||
emergencyPhoneController.dispose();
|
emergencyPhoneController.dispose();
|
||||||
relationshipController.dispose();
|
relationshipController.dispose();
|
||||||
|
|
||||||
|
@ -165,7 +160,7 @@ class StepFormController extends GetxController {
|
||||||
addressError.value = '';
|
addressError.value = '';
|
||||||
|
|
||||||
// Clear viewer-specific errors
|
// Clear viewer-specific errors
|
||||||
emergencyNameError.value = '';
|
nikError.value = '';
|
||||||
emergencyPhoneError.value = '';
|
emergencyPhoneError.value = '';
|
||||||
relationshipError.value = '';
|
relationshipError.value = '';
|
||||||
|
|
||||||
|
@ -215,11 +210,11 @@ class StepFormController extends GetxController {
|
||||||
|
|
||||||
final nameValidation = TValidators.validateUserInput(
|
final nameValidation = TValidators.validateUserInput(
|
||||||
'Emergency contact name',
|
'Emergency contact name',
|
||||||
emergencyNameController.text,
|
nikController.text,
|
||||||
100,
|
100,
|
||||||
);
|
);
|
||||||
if (nameValidation != null) {
|
if (nameValidation != null) {
|
||||||
emergencyNameError.value = nameValidation;
|
nikError.value = nameValidation;
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -376,7 +371,7 @@ class StepFormController extends GetxController {
|
||||||
} else {
|
} else {
|
||||||
// Viewer role
|
// Viewer role
|
||||||
final emergencyContact = {
|
final emergencyContact = {
|
||||||
'name': emergencyNameController.text,
|
'name': nikController.text,
|
||||||
'phone': emergencyPhoneController.text,
|
'phone': emergencyPhoneController.text,
|
||||||
'relationship': relationshipController.text,
|
'relationship': relationshipController.text,
|
||||||
};
|
};
|
||||||
|
|
|
@ -116,7 +116,7 @@ class EmailVerificationScreen extends StatelessWidget {
|
||||||
() => TextButton(
|
() => TextButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
controller.isResendEnabled.value
|
controller.isResendEnabled.value
|
||||||
? controller.resendCode
|
? () => controller.resendCode
|
||||||
: null,
|
: null,
|
||||||
child: Text(
|
child: Text(
|
||||||
controller.isResendEnabled.value
|
controller.isResendEnabled.value
|
||||||
|
|
|
@ -248,15 +248,15 @@ class StepFormScreen extends StatelessWidget {
|
||||||
// Emergency contact name field
|
// Emergency contact name field
|
||||||
Obx(
|
Obx(
|
||||||
() => CustomTextField(
|
() => CustomTextField(
|
||||||
label: 'Contact Name',
|
label: 'NIK',
|
||||||
controller: controller.emergencyNameController,
|
controller: controller.nikController,
|
||||||
validator:
|
validator:
|
||||||
(value) => TValidators.validateUserInput(
|
(value) => TValidators.validateUserInput(
|
||||||
'Emergency contact name',
|
'Emergency contact name',
|
||||||
value,
|
value,
|
||||||
100,
|
100,
|
||||||
),
|
),
|
||||||
errorText: controller.emergencyNameError.value,
|
errorText: controller.nikError.value,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -377,8 +377,8 @@ class StepFormScreen extends StatelessWidget {
|
||||||
controller.availableUnits
|
controller.availableUnits
|
||||||
.map(
|
.map(
|
||||||
(unit) => DropdownMenuItem(
|
(unit) => DropdownMenuItem(
|
||||||
value: unit['id'],
|
value: unit.codeUnit,
|
||||||
child: Text(unit['name']),
|
child: Text(unit.name),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
@ -391,7 +391,7 @@ class StepFormScreen extends StatelessWidget {
|
||||||
controller.unitIdController.text = value.toString();
|
controller.unitIdController.text = value.toString();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
validator: TValidators.validateUnitId,
|
validator: (value) => TValidators.validateUnitId(value),
|
||||||
errorText: controller.unitIdError.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:get/get.dart';
|
||||||
import 'package:sigap/src/cores/repositories/roles/roles_repository.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/constants/app_routes.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
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';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
|
||||||
class RoleCard extends StatelessWidget {
|
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 'officers_model.dart';
|
||||||
|
export 'permissions_model.dart';
|
||||||
export 'profile_model.dart';
|
export 'profile_model.dart';
|
||||||
|
export 'resources_model.dart';
|
||||||
export 'roles_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 TextInputType keyboardType;
|
||||||
final String? errorText;
|
final String? errorText;
|
||||||
final bool autofocus;
|
final bool autofocus;
|
||||||
|
final int maxLines;
|
||||||
final TextInputAction textInputAction;
|
final TextInputAction textInputAction;
|
||||||
final Function(String)? onChanged;
|
final Function(String)? onChanged;
|
||||||
|
|
||||||
|
@ -23,6 +24,7 @@ class CustomTextField extends StatelessWidget {
|
||||||
this.keyboardType = TextInputType.text,
|
this.keyboardType = TextInputType.text,
|
||||||
this.errorText,
|
this.errorText,
|
||||||
this.autofocus = false,
|
this.autofocus = false,
|
||||||
|
this.maxLines = 1,
|
||||||
this.textInputAction = TextInputAction.next,
|
this.textInputAction = TextInputAction.next,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
});
|
});
|
||||||
|
@ -48,6 +50,7 @@ class CustomTextField extends StatelessWidget {
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
autofocus: autofocus,
|
autofocus: autofocus,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
|
maxLines: maxLines,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
style: TextStyle(color: TColors.textPrimary, fontSize: 16),
|
style: TextStyle(color: TColors.textPrimary, fontSize: 16),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
|
|
|
@ -196,7 +196,7 @@ class TValidators {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? validateNRP(String? value) {
|
static String? validateNRP(String? value) {
|
||||||
return validateUserInput('NRP', value, 50, minLength: 5);
|
return validateUserInput('NRP', value, 50, minLength: 5);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.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 <gtk/gtk_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_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 =
|
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
|
||||||
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
|
flutter_secure_storage_linux
|
||||||
gtk
|
gtk
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,8 +10,10 @@ import connectivity_plus
|
||||||
import file_picker
|
import file_picker
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import flutter_local_notifications
|
import flutter_local_notifications
|
||||||
|
import flutter_secure_storage_macos
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import google_sign_in_ios
|
import google_sign_in_ios
|
||||||
|
import local_auth_darwin
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
@ -22,8 +24,10 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
|
||||||
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||||
|
FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
|
|
@ -427,6 +427,54 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.1"
|
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:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
@ -453,6 +501,38 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
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:
|
geolocator:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -781,6 +861,46 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
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:
|
logger:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -68,6 +68,7 @@ dependencies:
|
||||||
flutter_map:
|
flutter_map:
|
||||||
latlong2:
|
latlong2:
|
||||||
geolocator:
|
geolocator:
|
||||||
|
geocoding:
|
||||||
|
|
||||||
# Icons
|
# Icons
|
||||||
iconsax:
|
iconsax:
|
||||||
|
@ -82,7 +83,9 @@ dependencies:
|
||||||
supabase_flutter:
|
supabase_flutter:
|
||||||
|
|
||||||
# Authentication
|
# Authentication
|
||||||
google_sign_in:
|
google_sign_in:
|
||||||
|
local_auth:
|
||||||
|
flutter_secure_storage:
|
||||||
|
|
||||||
# API Services
|
# API Services
|
||||||
dio:
|
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 <app_links/app_links_plugin_c_api.h>
|
||||||
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
|
||||||
#include <file_selector_windows/file_selector_windows.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 <geolocator_windows/geolocator_windows.h>
|
||||||
|
#include <local_auth_windows/local_auth_plugin.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
|
@ -20,8 +22,12 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
|
||||||
FileSelectorWindowsRegisterWithRegistrar(
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
GeolocatorWindowsRegisterWithRegistrar(
|
GeolocatorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||||
|
LocalAuthPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("LocalAuthPlugin"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
|
|
@ -6,7 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
app_links
|
app_links
|
||||||
connectivity_plus
|
connectivity_plus
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
|
flutter_secure_storage_windows
|
||||||
geolocator_windows
|
geolocator_windows
|
||||||
|
local_auth_windows
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
|
@ -98,4 +98,12 @@ CREATE INDEX IF NOT EXISTS idx_units_location_district ON units (district_id, lo
|
||||||
|
|
||||||
-- Analisis tabel setelah membuat index
|
-- Analisis tabel setelah membuat index
|
||||||
ANALYZE units;
|
ANALYZE units;
|
||||||
ANALYZE locations;
|
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