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:
vergiLgood1 2025-05-16 18:41:16 +07:00
parent 7ad427baf6
commit ffed8b8ede
47 changed files with 3893 additions and 43 deletions

View File

@ -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
```

View File

@ -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());
} }

View File

@ -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 (_) {

View File

@ -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');
}
}
}

View File

@ -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()}');
}
}
}

View File

@ -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()}');
}
}
}

View File

@ -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()}');
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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 {
@ -11,6 +13,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,
}; };

View File

@ -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

View File

@ -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,
), ),
), ),

View File

@ -0,0 +1,3 @@
export 'patrol_units_model.dart';
export 'unit_statistics_model.dart';
export 'units_model.dart';

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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})';
}
}

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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';

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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';

View File

@ -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 {

View File

@ -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()})';
}
}

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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';

View File

@ -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)';
}
}

View File

@ -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';

View File

@ -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)';
}
}

View File

@ -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)';
}
}

View File

@ -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(

View File

@ -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);

View File

@ -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
) )

View File

@ -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"))

View File

@ -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:

View File

@ -68,6 +68,7 @@ dependencies:
flutter_map: flutter_map:
latlong2: latlong2:
geolocator: geolocator:
geocoding:
# Icons # Icons
iconsax: iconsax:
@ -83,6 +84,8 @@ dependencies:
# Authentication # Authentication
google_sign_in: google_sign_in:
local_auth:
flutter_secure_storage:
# API Services # API Services
dio: dio:

511
sigap-mobile/schema.prisma Normal file
View File

@ -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
}

View File

@ -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(

View File

@ -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
) )

View File

@ -99,3 +99,11 @@ 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 = '';