feat: Enhance KTP Validation Logic and Update Patrol Unit Selection UI
- Expanded KTP validation to include additional required fields: 'jenis_kelamin' and 'kewarganegaraan'. - Improved error handling for missing or invalid gender and nationality during KTP validation. - Updated Patrol Unit Selection Screen to reflect changes in UI elements, including renaming 'Position' to 'Rank' and enhancing dark mode support. - Refactored image uploader and circular loader components to utilize theme-aware colors for better visual consistency.
This commit is contained in:
parent
5bd92a0399
commit
105d992faa
|
@ -1411,7 +1411,7 @@ class AzureOCRService {
|
|||
// Check if KTP has all required fields
|
||||
bool isKtpValid(Map<String, String> extractedInfo) {
|
||||
// Required fields for KTP validation
|
||||
final requiredFields = ['nik', 'nama'];
|
||||
final requiredFields = ['nik', 'nama', 'jenis_kelamin', 'kewarganegaraan'];
|
||||
|
||||
// Check that all required fields are present and not empty
|
||||
for (var field in requiredFields) {
|
||||
|
@ -1439,12 +1439,12 @@ class AzureOCRService {
|
|||
// Birth place and date should be valid
|
||||
// final birthPlace = extractedInfo['tempat_lahir'] ?? '';
|
||||
// final birthDate = extractedInfo['tanggal_lahir'] ?? '';
|
||||
// if (birthPlace.isEmpty || birthDate.isEmpty) {
|
||||
// if (birthDate.isEmpty) {
|
||||
// print('KTP validation failed: missing birth place or date');
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// // Birth date should be in the format dd-mm-yyyy
|
||||
// // // Birth date should be in the format dd-mm-yyyy
|
||||
// final dateRegex = RegExp(r'^\d{1,2}-\d{1,2}-\d{2,4}$');
|
||||
// if (!dateRegex.hasMatch(birthDate)) {
|
||||
// print('KTP validation failed: invalid birth date format');
|
||||
|
@ -1452,19 +1452,19 @@ class AzureOCRService {
|
|||
// }
|
||||
|
||||
// // Gender should be either "L" or "P"
|
||||
// final gender = extractedInfo['jenis_kelamin'] ?? '';
|
||||
// if (gender.isEmpty || gender != 'LAKI-LAKI' || gender != 'PEREMPUAN') {
|
||||
// print('KTP validation failed: Missing or invalid gender');
|
||||
// return false;
|
||||
// }
|
||||
final gender = extractedInfo['jenis_kelamin'] ?? '';
|
||||
if (gender.isEmpty || gender != 'LAKI-LAKI' || gender != 'PEREMPUAN') {
|
||||
print('KTP validation failed: Missing or invalid gender');
|
||||
return false;
|
||||
}
|
||||
|
||||
// // Nationality should be "WNI"
|
||||
// final nationality = extractedInfo['kewarganegaraan'] ?? 'WNI';
|
||||
final nationality = extractedInfo['kewarganegaraan'] ?? 'WNI';
|
||||
|
||||
// if (nationality.isEmpty || nationality != 'WNI') {
|
||||
// print('KTP validation failed: Missing or invalid');
|
||||
// return false;
|
||||
// }
|
||||
if (nationality.isEmpty || nationality != 'WNI') {
|
||||
print('KTP validation failed: Missing or invalid');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validated successfully
|
||||
isValidKtp = true;
|
||||
|
|
|
@ -84,6 +84,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
|||
onClear: controller.clearIdCardImage,
|
||||
onValidate: controller.validateIdCardImage,
|
||||
placeholderIcon: Icons.add_a_photo,
|
||||
|
||||
),
|
||||
),
|
||||
|
||||
|
@ -187,7 +188,6 @@ class IdCardVerificationStep extends StatelessWidget {
|
|||
"Your photo and all text should be clearly visible",
|
||||
"Avoid using flash to prevent glare",
|
||||
],
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -285,7 +285,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
|||
_buildInfoRow('Unit', model.policeUnit, context),
|
||||
|
||||
if ((model.position ?? '').isNotEmpty)
|
||||
_buildInfoRow('Position', model.position ?? '', context),
|
||||
_buildInfoRow('Rank', model.position ?? '', context),
|
||||
|
||||
if (model.issueDate.isNotEmpty)
|
||||
_buildInfoRow('Issue Date', model.issueDate, context),
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/off
|
|||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
import 'package:sigap/src/utils/validators/validation.dart';
|
||||
|
||||
class PatrolUnitSelectionScreen extends StatelessWidget {
|
||||
|
@ -13,6 +14,7 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Configure Patrol Unit'),
|
||||
|
@ -30,12 +32,18 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
|
|||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
color:
|
||||
isDark
|
||||
? TColors.accent.withOpacity(0.1)
|
||||
: TColors.lightContainer.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.business, color: TColors.primary),
|
||||
Icon(
|
||||
Icons.business,
|
||||
color: isDark ? TColors.accent : TColors.primary,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
|
@ -43,9 +51,13 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
|
|||
children: [
|
||||
Text(
|
||||
'Selected Unit',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[600],
|
||||
fontSize: 12,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(
|
||||
color:
|
||||
isDark
|
||||
? TColors.accent.withOpacity(0.5)
|
||||
: TColors.primary.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
|
@ -70,94 +82,109 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(height: TSizes.spaceBtwInputFields / 2),
|
||||
GetX<OfficerInfoController>(
|
||||
builder:
|
||||
(controller) => Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
controller.setPatrolUnitType(PatrolUnitType.car);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
builder: (controller) {
|
||||
final isCarSelected =
|
||||
controller.selectedPatrolType.value == PatrolUnitType.car;
|
||||
final isMotorcycleSelected =
|
||||
controller.selectedPatrolType.value ==
|
||||
PatrolUnitType.motorcycle;
|
||||
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
controller.setPatrolUnitType(PatrolUnitType.car);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isCarSelected
|
||||
? isDark
|
||||
? TColors.accent.withOpacity(0.1)
|
||||
: TColors.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
controller.selectedPatrolType.value ==
|
||||
PatrolUnitType.car
|
||||
? TColors.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
isCarSelected
|
||||
? isDark
|
||||
? TColors.accent
|
||||
: TColors.primary
|
||||
: TColors.accent.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.directions_car,
|
||||
color:
|
||||
controller.selectedPatrolType.value ==
|
||||
PatrolUnitType.car
|
||||
? TColors.primary
|
||||
: Colors.grey,
|
||||
isCarSelected
|
||||
? isDark
|
||||
? TColors.accent
|
||||
: TColors.primary
|
||||
: TColors.accent.withOpacity(0.3),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.directions_car,
|
||||
color:
|
||||
controller.selectedPatrolType.value ==
|
||||
PatrolUnitType.car
|
||||
? TColors.primary
|
||||
: Colors.grey,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
const Text('Car'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
const Text('Car'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: TSizes.spaceBtwInputFields),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
controller.setPatrolUnitType(
|
||||
PatrolUnitType.motorcycle,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
),
|
||||
const SizedBox(width: TSizes.spaceBtwInputFields),
|
||||
Expanded(
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
controller.setPatrolUnitType(
|
||||
PatrolUnitType.motorcycle,
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isMotorcycleSelected
|
||||
? isDark
|
||||
? TColors.accent.withOpacity(0.1)
|
||||
: TColors.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
controller.selectedPatrolType.value ==
|
||||
PatrolUnitType.motorcycle
|
||||
? TColors.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
isMotorcycleSelected
|
||||
? isDark
|
||||
? TColors.accent
|
||||
: TColors.primary
|
||||
: TColors.accent.withOpacity(0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.motorcycle,
|
||||
color:
|
||||
controller.selectedPatrolType.value ==
|
||||
PatrolUnitType.motorcycle
|
||||
? TColors.primary
|
||||
: Colors.grey,
|
||||
isMotorcycleSelected
|
||||
? isDark
|
||||
? TColors.accent
|
||||
: TColors.primary
|
||||
: TColors.accent.withOpacity(0.3),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.motorcycle,
|
||||
color:
|
||||
controller.selectedPatrolType.value ==
|
||||
PatrolUnitType.motorcycle
|
||||
? TColors.primary
|
||||
: Colors.grey,
|
||||
size: 32,
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
const Text('Motorcycle'),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
const Text('Motorcycle'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: TSizes.spaceBtwSections),
|
||||
|
@ -177,18 +204,26 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
|
|||
PatrolSelectionMode.individual,
|
||||
'Individual',
|
||||
Icons.person,
|
||||
context,
|
||||
),
|
||||
|
||||
const SizedBox(width: TSizes.spaceBtwInputFields),
|
||||
|
||||
_buildModeTab(
|
||||
controller,
|
||||
PatrolSelectionMode.group,
|
||||
'Group',
|
||||
Icons.group,
|
||||
context,
|
||||
),
|
||||
const SizedBox(width: TSizes.spaceBtwInputFields),
|
||||
|
||||
_buildModeTab(
|
||||
controller,
|
||||
PatrolSelectionMode.createNew,
|
||||
'Create New',
|
||||
Icons.add_circle,
|
||||
context,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -264,44 +299,39 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
|
|||
PatrolSelectionMode mode,
|
||||
String label,
|
||||
IconData icon,
|
||||
BuildContext context,
|
||||
) {
|
||||
final bool isSelected = controller.patrolSelectionMode.value == mode;
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
// define the color based on selection and theme
|
||||
final boxDecorationColor =
|
||||
isSelected
|
||||
? (isDark
|
||||
? TColors.accent.withOpacity(0.1)
|
||||
: TColors.primary.withOpacity(0.1))
|
||||
: TColors.transparent;
|
||||
|
||||
final color =
|
||||
isSelected
|
||||
? (isDark ? TColors.accent : TColors.primary)
|
||||
: TColors.accent.withOpacity(0.3);
|
||||
|
||||
return Expanded(
|
||||
child: InkWell(
|
||||
onTap: () => controller.setPatrolSelectionMode(mode),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
controller.patrolSelectionMode.value == mode
|
||||
? TColors.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
color: boxDecorationColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color:
|
||||
controller.patrolSelectionMode.value == mode
|
||||
? TColors.primary
|
||||
: Colors.grey,
|
||||
),
|
||||
border: Border.all(color: color),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color:
|
||||
controller.patrolSelectionMode.value == mode
|
||||
? TColors.primary
|
||||
: Colors.grey,
|
||||
),
|
||||
Icon(icon, color: color),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color:
|
||||
controller.patrolSelectionMode.value == mode
|
||||
? TColors.primary
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
Text(label, style: TextStyle(color: color)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -348,38 +378,42 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
|
|||
itemCount: filteredUnits.length,
|
||||
itemBuilder: (context, index) {
|
||||
final patrolUnit = filteredUnits[index];
|
||||
final isSelected =
|
||||
final isUnitSelected =
|
||||
patrolUnit.id == controller.patrolUnitIdController.text;
|
||||
final isCarType = patrolUnit.type.toLowerCase() == 'car';
|
||||
|
||||
return Card(
|
||||
elevation: isSelected ? 2 : 0,
|
||||
color: isSelected ? TColors.primary.withOpacity(0.1) : null,
|
||||
elevation: isUnitSelected ? 2 : 0,
|
||||
color:
|
||||
isUnitSelected
|
||||
? TColors.primary.withOpacity(0.1)
|
||||
: null,
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: ListTile(
|
||||
title: Text(
|
||||
patrolUnit.name,
|
||||
style: TextStyle(
|
||||
fontWeight:
|
||||
isSelected ? FontWeight.bold : FontWeight.normal,
|
||||
isUnitSelected
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'Members: ${patrolUnit.members?.length ?? 0}',
|
||||
),
|
||||
leading: Icon(
|
||||
patrolUnit.type.toLowerCase() == 'car'
|
||||
? Icons.directions_car
|
||||
: Icons.motorcycle,
|
||||
color: isSelected ? TColors.primary : null,
|
||||
isCarType ? Icons.directions_car : Icons.motorcycle,
|
||||
color: isUnitSelected ? TColors.primary : null,
|
||||
),
|
||||
trailing:
|
||||
isSelected
|
||||
isUnitSelected
|
||||
? const Icon(
|
||||
Icons.check_circle,
|
||||
color: TColors.primary,
|
||||
)
|
||||
: null,
|
||||
selected: isSelected,
|
||||
selected: isUnitSelected,
|
||||
onTap: () => controller.joinPatrolUnit(patrolUnit),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
import 'package:sigap/src/utils/loaders/circular_loader.dart';
|
||||
|
||||
class ImageUploader extends StatelessWidget {
|
||||
|
@ -68,7 +69,7 @@ class ImageUploader extends StatelessWidget {
|
|||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (image == null)
|
||||
_buildEmptyUploader(backgroundColor, borderColor)
|
||||
_buildEmptyUploader(backgroundColor, borderColor, context)
|
||||
else
|
||||
_buildImagePreview(borderColor, context),
|
||||
|
||||
|
@ -102,7 +103,13 @@ class ImageUploader extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyUploader(Color backgroundColor, Color borderColor) {
|
||||
Widget _buildEmptyUploader(
|
||||
Color backgroundColor,
|
||||
Color borderColor,
|
||||
BuildContext context,
|
||||
) {
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTapToSelect,
|
||||
child: Container(
|
||||
|
@ -122,7 +129,11 @@ class ImageUploader extends StatelessWidget {
|
|||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const TCircularLoader(),
|
||||
TCircularLoader(
|
||||
backgroundColor: TColors.transparent,
|
||||
foregroundColor:
|
||||
isDark ? TColors.accent : TColors.primary,
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
Text(
|
||||
'Uploading...',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||
import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||
|
||||
/// A circular loader widget with customizable foreground and background colors.
|
||||
class TCircularLoader extends StatelessWidget {
|
||||
|
@ -20,6 +21,7 @@ class TCircularLoader extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = THelperFunctions.isDarkMode(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(TSizes.lg),
|
||||
decoration: BoxDecoration(
|
||||
|
|
|
@ -11,17 +11,18 @@ datasource db {
|
|||
}
|
||||
|
||||
model profiles {
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
user_id String @unique @db.Uuid
|
||||
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?
|
||||
nik String? @default("") @db.VarChar(100)
|
||||
users users @relation(fields: [user_id], references: [id])
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
user_id String @unique @db.Uuid
|
||||
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?
|
||||
nik String? @db.VarChar(100)
|
||||
birth_place String?
|
||||
users users @relation(fields: [user_id], references: [id])
|
||||
|
||||
@@index([user_id])
|
||||
@@index([username])
|
||||
|
@ -321,17 +322,19 @@ model units {
|
|||
}
|
||||
|
||||
model patrol_units {
|
||||
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)
|
||||
id String @id @unique @db.VarChar(100)
|
||||
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)
|
||||
unit_id String @db.VarChar(20)
|
||||
location_id String @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
type String @db.VarChar(50)
|
||||
category patrol_unit_category? @default(group)
|
||||
member_count Int? @default(0)
|
||||
status String @db.VarChar(50)
|
||||
radius Float
|
||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||
id String @id @unique @db.VarChar(100)
|
||||
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")
|
||||
|
@ -341,9 +344,9 @@ model patrol_units {
|
|||
}
|
||||
|
||||
model officers {
|
||||
unit_id String @db.VarChar(20)
|
||||
unit_id String? @db.VarChar(20)
|
||||
role_id String @db.Uuid
|
||||
nrp String @unique @db.VarChar(100)
|
||||
nrp String? @unique @db.VarChar(100)
|
||||
name String @db.VarChar(100)
|
||||
rank String? @db.VarChar(100)
|
||||
position String? @db.VarChar(100)
|
||||
|
@ -354,7 +357,7 @@ model officers {
|
|||
qr_code String?
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
patrol_unit_id String @db.VarChar(100)
|
||||
patrol_unit_id String? @db.VarChar(100)
|
||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||
banned_reason String? @db.VarChar(255)
|
||||
banned_until DateTime?
|
||||
|
@ -363,9 +366,9 @@ model officers {
|
|||
spoofing_attempts Int @default(0)
|
||||
place_of_birth String?
|
||||
date_of_birth DateTime? @db.Timestamptz(6)
|
||||
patrol_units patrol_units @relation(fields: [patrol_unit_id], references: [id])
|
||||
patrol_units patrol_units? @relation(fields: [patrol_unit_id], references: [id], onDelete: Restrict)
|
||||
roles roles @relation(fields: [role_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
units units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||
units units? @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||
panic_button_logs panic_button_logs[]
|
||||
|
||||
@@index([unit_id], map: "idx_officers_unit_id")
|
||||
|
@ -472,6 +475,11 @@ model location_logs {
|
|||
@@index([user_id], map: "idx_location_logs_user_id")
|
||||
}
|
||||
|
||||
enum patrol_unit_category {
|
||||
individual
|
||||
group
|
||||
}
|
||||
|
||||
enum session_status {
|
||||
active
|
||||
completed
|
||||
|
|
|
@ -33,19 +33,19 @@ class DatabaseSeeder {
|
|||
|
||||
// Daftar semua seeders di sini
|
||||
this.seeders = [
|
||||
new RoleSeeder(prisma),
|
||||
new ResourceSeeder(prisma),
|
||||
new PermissionSeeder(prisma),
|
||||
new CrimeCategoriesSeeder(prisma),
|
||||
new GeoJSONSeeder(prisma),
|
||||
new UnitSeeder(prisma),
|
||||
// new RoleSeeder(prisma),
|
||||
// new ResourceSeeder(prisma),
|
||||
// new PermissionSeeder(prisma),
|
||||
// new CrimeCategoriesSeeder(prisma),
|
||||
// new GeoJSONSeeder(prisma),
|
||||
// new UnitSeeder(prisma),
|
||||
new PatrolUnitsSeeder(prisma),
|
||||
new OfficersSeeder(prisma),
|
||||
new DemographicsSeeder(prisma),
|
||||
new CrimesSeeder(prisma),
|
||||
// new OfficersSeeder(prisma),
|
||||
// new DemographicsSeeder(prisma),
|
||||
// new CrimesSeeder(prisma),
|
||||
// new CrimeIncidentsByUnitSeeder(prisma),
|
||||
new CrimeIncidentsByTypeSeeder(prisma),
|
||||
new IncidentLogSeeder(prisma),
|
||||
// new CrimeIncidentsByTypeSeeder(prisma),
|
||||
// new IncidentLogSeeder(prisma),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -11,9 +11,85 @@ export class PatrolUnitsSeeder {
|
|||
private supabase = createClient()
|
||||
) { }
|
||||
|
||||
async run(): Promise<void> {
|
||||
// Add tactical callsigns as a class property
|
||||
private tacticalCallsigns = [
|
||||
'Alpha', 'Bravo', 'Charlie', 'Delta', 'Echo',
|
||||
'Foxtrot', 'Ghost', 'Hunter', 'Ice', 'Jaguar',
|
||||
'Kilo', 'Lima', 'Mike', 'Nova', 'Omega',
|
||||
'Phoenix', 'Quebec', 'Romeo', 'Sierra', 'Tango',
|
||||
'Ultra', 'Victor', 'Whiskey', 'X-Ray', 'Yankee',
|
||||
'Zulu', 'Raptor', 'Viper', 'Cobra', 'Eagle',
|
||||
];
|
||||
|
||||
// Mapping type to code
|
||||
private typeCodeMap: Record<string, string> = {
|
||||
car: "C",
|
||||
motorcycle: "M",
|
||||
foot: "F",
|
||||
mixed: "X",
|
||||
drone: "D",
|
||||
};
|
||||
|
||||
// Helper method to get random callsign
|
||||
private getRandomCallsign(): string {
|
||||
return this.tacticalCallsigns[Math.floor(Math.random() * this.tacticalCallsigns.length)];
|
||||
}
|
||||
|
||||
async run(isUpdate: boolean = true): Promise<void> {
|
||||
console.log('🚓 Seeding patrol units...');
|
||||
|
||||
if (isUpdate) {
|
||||
console.log('Updating patrol unit categories based on member count...');
|
||||
|
||||
// Get all patrol units with their member counts
|
||||
const patrolUnitsWithMembers = await this.prisma.patrol_units.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
unit_id: true,
|
||||
type: true,
|
||||
_count: {
|
||||
select: {
|
||||
members: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update each patrol unit's member_count and category
|
||||
for (const pu of patrolUnitsWithMembers) {
|
||||
const memberCount = pu._count.members;
|
||||
const category = memberCount > 1 ? 'group' : 'individual';
|
||||
const callsign = this.getRandomCallsign();
|
||||
const patrolName = `${callsign}-${pu.id.split('-')[1]}`;
|
||||
|
||||
try {
|
||||
await this.prisma.patrol_units.update({
|
||||
where: { id: pu.id },
|
||||
data: {
|
||||
member_count: memberCount,
|
||||
category: category,
|
||||
name: patrolName.toUpperCase()
|
||||
}
|
||||
});
|
||||
|
||||
// Update in Supabase as well
|
||||
// await this.supabase
|
||||
// .from('patrol_units')
|
||||
// .update({
|
||||
// member_count: memberCount,
|
||||
// category: category
|
||||
// })
|
||||
// .eq('id', pu.id);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Error updating patrol unit ${pu.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✅ Updated patrol unit categories and member counts');
|
||||
return;
|
||||
}
|
||||
|
||||
// First, let's clear existing patrol units
|
||||
try {
|
||||
await this.prisma.patrol_units.deleteMany({});
|
||||
|
@ -21,7 +97,7 @@ export class PatrolUnitsSeeder {
|
|||
await this.supabase.from('patrol_units').delete().neq('id', 'dummy');
|
||||
console.log('✅ Removed existing patrol units');
|
||||
} catch (error) {
|
||||
console.error('❌ Error removing existing patrol units:', error);
|
||||
console.error('❌k Error removing existing patrol units:', error);
|
||||
return; // Exit if we can't clean up properly
|
||||
}
|
||||
|
||||
|
@ -84,15 +160,6 @@ export class PatrolUnitsSeeder {
|
|||
}
|
||||
};
|
||||
|
||||
// Mapping type to code
|
||||
const typeCodeMap: Record<string, string> = {
|
||||
car: "C",
|
||||
motorcycle: "M",
|
||||
foot: "F",
|
||||
mixed: "X",
|
||||
drone: "D",
|
||||
};
|
||||
|
||||
// Get locations for each district to assign to patrol units
|
||||
const locationsByDistrict = await this.getLocationsByDistrict();
|
||||
|
||||
|
@ -111,9 +178,8 @@ export class PatrolUnitsSeeder {
|
|||
for (let i = 1; i <= patrolCount; i++) {
|
||||
// Select patrol type based on weighted distribution
|
||||
const patrolType = this.getWeightedRandomItem(unitTypeWeights) as string;
|
||||
|
||||
const patrolName = `${unit.name.replace('Polsek', 'Patroli').replace('Polres', 'Patroli')} ${patrolType.charAt(0).toUpperCase() + patrolType.slice(1)
|
||||
} ${i}`;
|
||||
const callsign = this.getRandomCallsign();
|
||||
const patrolName = `${callsign}-${unit.code_unit}-${this.typeCodeMap[patrolType]}${i}`;
|
||||
|
||||
const radius = getPatrolRadius(patrolType);
|
||||
const status = this.getWeightedRandomItem(weightedStatus) as string;
|
||||
|
@ -132,7 +198,7 @@ export class PatrolUnitsSeeder {
|
|||
continue;
|
||||
}
|
||||
|
||||
const typeCode = typeCodeMap[patrolType] || "P";
|
||||
const typeCode = this.typeCodeMap[patrolType] || "P";
|
||||
const codeUnitLast2 = unit.code_unit.slice(-2);
|
||||
|
||||
try {
|
||||
|
@ -149,6 +215,10 @@ export class PatrolUnitsSeeder {
|
|||
CRegex.PATROL_UNIT_ID_REGEX
|
||||
);
|
||||
|
||||
// Generate random member count and set category
|
||||
const memberCount = faker.number.int({ min: 1, max: 4 });
|
||||
const category = memberCount > 1 ? 'group' : 'individual';
|
||||
|
||||
patrolUnits.push({
|
||||
id: newId,
|
||||
unit_id: unit.code_unit,
|
||||
|
@ -157,6 +227,8 @@ export class PatrolUnitsSeeder {
|
|||
type: patrolType,
|
||||
status: status,
|
||||
radius: radius,
|
||||
member_count: memberCount,
|
||||
category: category
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error generating ID for patrol unit: ${error}`);
|
||||
|
@ -218,7 +290,7 @@ export class PatrolUnitsSeeder {
|
|||
where: {
|
||||
user_id: user.id
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (!event) {
|
||||
try {
|
||||
|
|
Loading…
Reference in New Issue