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:
vergiLgood1 2025-05-26 16:22:52 +07:00
parent 5bd92a0399
commit 105d992faa
8 changed files with 318 additions and 191 deletions

View File

@ -1411,7 +1411,7 @@ class AzureOCRService {
// Check if KTP has all required fields // Check if KTP has all required fields
bool isKtpValid(Map<String, String> extractedInfo) { bool isKtpValid(Map<String, String> extractedInfo) {
// Required fields for KTP validation // 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 // Check that all required fields are present and not empty
for (var field in requiredFields) { for (var field in requiredFields) {
@ -1439,12 +1439,12 @@ class AzureOCRService {
// Birth place and date should be valid // Birth place and date should be valid
// final birthPlace = extractedInfo['tempat_lahir'] ?? ''; // final birthPlace = extractedInfo['tempat_lahir'] ?? '';
// final birthDate = extractedInfo['tanggal_lahir'] ?? ''; // final birthDate = extractedInfo['tanggal_lahir'] ?? '';
// if (birthPlace.isEmpty || birthDate.isEmpty) { // if (birthDate.isEmpty) {
// print('KTP validation failed: missing birth place or date'); // print('KTP validation failed: missing birth place or date');
// return false; // 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}$'); // final dateRegex = RegExp(r'^\d{1,2}-\d{1,2}-\d{2,4}$');
// if (!dateRegex.hasMatch(birthDate)) { // if (!dateRegex.hasMatch(birthDate)) {
// print('KTP validation failed: invalid birth date format'); // print('KTP validation failed: invalid birth date format');
@ -1452,19 +1452,19 @@ class AzureOCRService {
// } // }
// // Gender should be either "L" or "P" // // Gender should be either "L" or "P"
// final gender = extractedInfo['jenis_kelamin'] ?? ''; final gender = extractedInfo['jenis_kelamin'] ?? '';
// if (gender.isEmpty || gender != 'LAKI-LAKI' || gender != 'PEREMPUAN') { if (gender.isEmpty || gender != 'LAKI-LAKI' || gender != 'PEREMPUAN') {
// print('KTP validation failed: Missing or invalid gender'); print('KTP validation failed: Missing or invalid gender');
// return false; return false;
// } }
// // Nationality should be "WNI" // // Nationality should be "WNI"
// final nationality = extractedInfo['kewarganegaraan'] ?? 'WNI'; final nationality = extractedInfo['kewarganegaraan'] ?? 'WNI';
// if (nationality.isEmpty || nationality != 'WNI') { if (nationality.isEmpty || nationality != 'WNI') {
// print('KTP validation failed: Missing or invalid'); print('KTP validation failed: Missing or invalid');
// return false; return false;
// } }
// Validated successfully // Validated successfully
isValidKtp = true; isValidKtp = true;

View File

@ -84,6 +84,7 @@ class IdCardVerificationStep extends StatelessWidget {
onClear: controller.clearIdCardImage, onClear: controller.clearIdCardImage,
onValidate: controller.validateIdCardImage, onValidate: controller.validateIdCardImage,
placeholderIcon: Icons.add_a_photo, placeholderIcon: Icons.add_a_photo,
), ),
), ),
@ -187,7 +188,6 @@ class IdCardVerificationStep extends StatelessWidget {
"Your photo and all text should be clearly visible", "Your photo and all text should be clearly visible",
"Avoid using flash to prevent glare", "Avoid using flash to prevent glare",
], ],
); );
} }
@ -285,7 +285,7 @@ class IdCardVerificationStep extends StatelessWidget {
_buildInfoRow('Unit', model.policeUnit, context), _buildInfoRow('Unit', model.policeUnit, context),
if ((model.position ?? '').isNotEmpty) if ((model.position ?? '').isNotEmpty)
_buildInfoRow('Position', model.position ?? '', context), _buildInfoRow('Rank', model.position ?? '', context),
if (model.issueDate.isNotEmpty) if (model.issueDate.isNotEmpty)
_buildInfoRow('Issue Date', model.issueDate, context), _buildInfoRow('Issue Date', model.issueDate, context),

View File

@ -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/shared/widgets/text/custom_text_field.dart';
import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.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'; import 'package:sigap/src/utils/validators/validation.dart';
class PatrolUnitSelectionScreen extends StatelessWidget { class PatrolUnitSelectionScreen extends StatelessWidget {
@ -13,6 +14,7 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = THelperFunctions.isDarkMode(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Configure Patrol Unit'), title: const Text('Configure Patrol Unit'),
@ -30,12 +32,18 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
Container( Container(
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey[100], color:
isDark
? TColors.accent.withOpacity(0.1)
: TColors.lightContainer.withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Row( child: Row(
children: [ children: [
const Icon(Icons.business, color: TColors.primary), Icon(
Icons.business,
color: isDark ? TColors.accent : TColors.primary,
),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
@ -43,9 +51,13 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
children: [ children: [
Text( Text(
'Selected Unit', 'Selected Unit',
style: TextStyle( style: Theme.of(
color: Colors.grey[600], context,
fontSize: 12, ).textTheme.labelSmall?.copyWith(
color:
isDark
? TColors.accent.withOpacity(0.5)
: TColors.primary.withOpacity(0.5),
), ),
), ),
Obx( Obx(
@ -70,8 +82,16 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
), ),
const SizedBox(height: TSizes.spaceBtwInputFields / 2), const SizedBox(height: TSizes.spaceBtwInputFields / 2),
GetX<OfficerInfoController>( GetX<OfficerInfoController>(
builder: builder: (controller) {
(controller) => Row( final isCarSelected =
controller.selectedPatrolType.value == PatrolUnitType.car;
final isMotorcycleSelected =
controller.selectedPatrolType.value ==
PatrolUnitType.motorcycle;
final isDark = THelperFunctions.isDarkMode(context);
return Row(
children: [ children: [
Expanded( Expanded(
child: InkWell( child: InkWell(
@ -82,17 +102,19 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color:
controller.selectedPatrolType.value == isCarSelected
PatrolUnitType.car ? isDark
? TColors.primary.withOpacity(0.1) ? TColors.accent.withOpacity(0.1)
: TColors.primary.withOpacity(0.1)
: Colors.transparent, : Colors.transparent,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: color:
controller.selectedPatrolType.value == isCarSelected
PatrolUnitType.car ? isDark
? TColors.primary ? TColors.accent
: Colors.grey, : TColors.primary
: TColors.accent.withOpacity(0.3),
), ),
), ),
child: Column( child: Column(
@ -100,10 +122,11 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
Icon( Icon(
Icons.directions_car, Icons.directions_car,
color: color:
controller.selectedPatrolType.value == isCarSelected
PatrolUnitType.car ? isDark
? TColors.primary ? TColors.accent
: Colors.grey, : TColors.primary
: TColors.accent.withOpacity(0.3),
size: 32, size: 32,
), ),
const SizedBox(height: TSizes.sm), const SizedBox(height: TSizes.sm),
@ -125,17 +148,19 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color:
controller.selectedPatrolType.value == isMotorcycleSelected
PatrolUnitType.motorcycle ? isDark
? TColors.primary.withOpacity(0.1) ? TColors.accent.withOpacity(0.1)
: TColors.primary.withOpacity(0.1)
: Colors.transparent, : Colors.transparent,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(
color: color:
controller.selectedPatrolType.value == isMotorcycleSelected
PatrolUnitType.motorcycle ? isDark
? TColors.primary ? TColors.accent
: Colors.grey, : TColors.primary
: TColors.accent.withOpacity(0.3),
), ),
), ),
child: Column( child: Column(
@ -143,10 +168,11 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
Icon( Icon(
Icons.motorcycle, Icons.motorcycle,
color: color:
controller.selectedPatrolType.value == isMotorcycleSelected
PatrolUnitType.motorcycle ? isDark
? TColors.primary ? TColors.accent
: Colors.grey, : TColors.primary
: TColors.accent.withOpacity(0.3),
size: 32, size: 32,
), ),
const SizedBox(height: TSizes.sm), const SizedBox(height: TSizes.sm),
@ -157,7 +183,8 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
), ),
), ),
], ],
), );
},
), ),
const SizedBox(height: TSizes.spaceBtwSections), const SizedBox(height: TSizes.spaceBtwSections),
@ -177,18 +204,26 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
PatrolSelectionMode.individual, PatrolSelectionMode.individual,
'Individual', 'Individual',
Icons.person, Icons.person,
context,
), ),
const SizedBox(width: TSizes.spaceBtwInputFields),
_buildModeTab( _buildModeTab(
controller, controller,
PatrolSelectionMode.group, PatrolSelectionMode.group,
'Group', 'Group',
Icons.group, Icons.group,
context,
), ),
const SizedBox(width: TSizes.spaceBtwInputFields),
_buildModeTab( _buildModeTab(
controller, controller,
PatrolSelectionMode.createNew, PatrolSelectionMode.createNew,
'Create New', 'Create New',
Icons.add_circle, Icons.add_circle,
context,
), ),
], ],
), ),
@ -264,44 +299,39 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
PatrolSelectionMode mode, PatrolSelectionMode mode,
String label, String label,
IconData icon, 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( return Expanded(
child: InkWell( child: InkWell(
onTap: () => controller.setPatrolSelectionMode(mode), onTap: () => controller.setPatrolSelectionMode(mode),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: boxDecorationColor,
controller.patrolSelectionMode.value == mode
? TColors.primary.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
border: Border.all( border: Border.all(color: color),
color:
controller.patrolSelectionMode.value == mode
? TColors.primary
: Colors.grey,
),
), ),
child: Column( child: Column(
children: [ children: [
Icon( Icon(icon, color: color),
icon,
color:
controller.patrolSelectionMode.value == mode
? TColors.primary
: Colors.grey,
),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(label, style: TextStyle(color: color)),
label,
style: TextStyle(
color:
controller.patrolSelectionMode.value == mode
? TColors.primary
: Colors.grey,
),
),
], ],
), ),
), ),
@ -348,38 +378,42 @@ class PatrolUnitSelectionScreen extends StatelessWidget {
itemCount: filteredUnits.length, itemCount: filteredUnits.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final patrolUnit = filteredUnits[index]; final patrolUnit = filteredUnits[index];
final isSelected = final isUnitSelected =
patrolUnit.id == controller.patrolUnitIdController.text; patrolUnit.id == controller.patrolUnitIdController.text;
final isCarType = patrolUnit.type.toLowerCase() == 'car';
return Card( return Card(
elevation: isSelected ? 2 : 0, elevation: isUnitSelected ? 2 : 0,
color: isSelected ? TColors.primary.withOpacity(0.1) : null, color:
isUnitSelected
? TColors.primary.withOpacity(0.1)
: null,
margin: const EdgeInsets.symmetric(vertical: 4), margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile( child: ListTile(
title: Text( title: Text(
patrolUnit.name, patrolUnit.name,
style: TextStyle( style: TextStyle(
fontWeight: fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal, isUnitSelected
? FontWeight.bold
: FontWeight.normal,
), ),
), ),
subtitle: Text( subtitle: Text(
'Members: ${patrolUnit.members?.length ?? 0}', 'Members: ${patrolUnit.members?.length ?? 0}',
), ),
leading: Icon( leading: Icon(
patrolUnit.type.toLowerCase() == 'car' isCarType ? Icons.directions_car : Icons.motorcycle,
? Icons.directions_car color: isUnitSelected ? TColors.primary : null,
: Icons.motorcycle,
color: isSelected ? TColors.primary : null,
), ),
trailing: trailing:
isSelected isUnitSelected
? const Icon( ? const Icon(
Icons.check_circle, Icons.check_circle,
color: TColors.primary, color: TColors.primary,
) )
: null, : null,
selected: isSelected, selected: isUnitSelected,
onTap: () => controller.joinPatrolUnit(patrolUnit), onTap: () => controller.joinPatrolUnit(patrolUnit),
), ),
); );

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.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'; import 'package:sigap/src/utils/loaders/circular_loader.dart';
class ImageUploader extends StatelessWidget { class ImageUploader extends StatelessWidget {
@ -68,7 +69,7 @@ class ImageUploader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
if (image == null) if (image == null)
_buildEmptyUploader(backgroundColor, borderColor) _buildEmptyUploader(backgroundColor, borderColor, context)
else else
_buildImagePreview(borderColor, context), _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( return GestureDetector(
onTap: onTapToSelect, onTap: onTapToSelect,
child: Container( child: Container(
@ -122,7 +129,11 @@ class ImageUploader extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const TCircularLoader(), TCircularLoader(
backgroundColor: TColors.transparent,
foregroundColor:
isDark ? TColors.accent : TColors.primary,
),
const SizedBox(height: TSizes.sm), const SizedBox(height: TSizes.sm),
Text( Text(
'Uploading...', 'Uploading...',

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.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. /// A circular loader widget with customizable foreground and background colors.
class TCircularLoader extends StatelessWidget { class TCircularLoader extends StatelessWidget {
@ -20,6 +21,7 @@ class TCircularLoader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isDark = THelperFunctions.isDarkMode(context);
return Container( return Container(
padding: const EdgeInsets.all(TSizes.lg), padding: const EdgeInsets.all(TSizes.lg),
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -20,7 +20,8 @@ model profiles {
bio String? @db.VarChar bio String? @db.VarChar
address Json? @db.Json address Json? @db.Json
birth_date DateTime? birth_date DateTime?
nik String? @default("") @db.VarChar(100) nik String? @db.VarChar(100)
birth_place String?
users users @relation(fields: [user_id], references: [id]) users users @relation(fields: [user_id], references: [id])
@@index([user_id]) @@index([user_id])
@ -325,6 +326,8 @@ model patrol_units {
location_id String @db.Uuid location_id String @db.Uuid
name String @db.VarChar(100) name String @db.VarChar(100)
type String @db.VarChar(50) type String @db.VarChar(50)
category patrol_unit_category? @default(group)
member_count Int? @default(0)
status String @db.VarChar(50) status String @db.VarChar(50)
radius Float radius Float
created_at DateTime @default(now()) @db.Timestamptz(6) created_at DateTime @default(now()) @db.Timestamptz(6)
@ -341,9 +344,9 @@ model patrol_units {
} }
model officers { model officers {
unit_id String @db.VarChar(20) unit_id String? @db.VarChar(20)
role_id String @db.Uuid role_id String @db.Uuid
nrp String @unique @db.VarChar(100) nrp String? @unique @db.VarChar(100)
name String @db.VarChar(100) name String @db.VarChar(100)
rank String? @db.VarChar(100) rank String? @db.VarChar(100)
position String? @db.VarChar(100) position String? @db.VarChar(100)
@ -354,7 +357,7 @@ model officers {
qr_code String? qr_code String?
created_at DateTime? @default(now()) @db.Timestamptz(6) created_at DateTime? @default(now()) @db.Timestamptz(6)
updated_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 id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
banned_reason String? @db.VarChar(255) banned_reason String? @db.VarChar(255)
banned_until DateTime? banned_until DateTime?
@ -363,9 +366,9 @@ model officers {
spoofing_attempts Int @default(0) spoofing_attempts Int @default(0)
place_of_birth String? place_of_birth String?
date_of_birth DateTime? @db.Timestamptz(6) 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) 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[] panic_button_logs panic_button_logs[]
@@index([unit_id], map: "idx_officers_unit_id") @@index([unit_id], map: "idx_officers_unit_id")
@ -472,6 +475,11 @@ model location_logs {
@@index([user_id], map: "idx_location_logs_user_id") @@index([user_id], map: "idx_location_logs_user_id")
} }
enum patrol_unit_category {
individual
group
}
enum session_status { enum session_status {
active active
completed completed

View File

@ -33,19 +33,19 @@ class DatabaseSeeder {
// Daftar semua seeders di sini // Daftar semua seeders di sini
this.seeders = [ this.seeders = [
new RoleSeeder(prisma), // new RoleSeeder(prisma),
new ResourceSeeder(prisma), // new ResourceSeeder(prisma),
new PermissionSeeder(prisma), // new PermissionSeeder(prisma),
new CrimeCategoriesSeeder(prisma), // new CrimeCategoriesSeeder(prisma),
new GeoJSONSeeder(prisma), // new GeoJSONSeeder(prisma),
new UnitSeeder(prisma), // new UnitSeeder(prisma),
new PatrolUnitsSeeder(prisma), new PatrolUnitsSeeder(prisma),
new OfficersSeeder(prisma), // new OfficersSeeder(prisma),
new DemographicsSeeder(prisma), // new DemographicsSeeder(prisma),
new CrimesSeeder(prisma), // new CrimesSeeder(prisma),
// new CrimeIncidentsByUnitSeeder(prisma), // new CrimeIncidentsByUnitSeeder(prisma),
new CrimeIncidentsByTypeSeeder(prisma), // new CrimeIncidentsByTypeSeeder(prisma),
new IncidentLogSeeder(prisma), // new IncidentLogSeeder(prisma),
]; ];
} }

View File

@ -11,9 +11,85 @@ export class PatrolUnitsSeeder {
private supabase = createClient() 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...'); 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 // First, let's clear existing patrol units
try { try {
await this.prisma.patrol_units.deleteMany({}); await this.prisma.patrol_units.deleteMany({});
@ -21,7 +97,7 @@ export class PatrolUnitsSeeder {
await this.supabase.from('patrol_units').delete().neq('id', 'dummy'); await this.supabase.from('patrol_units').delete().neq('id', 'dummy');
console.log('✅ Removed existing patrol units'); console.log('✅ Removed existing patrol units');
} catch (error) { } 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 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 // Get locations for each district to assign to patrol units
const locationsByDistrict = await this.getLocationsByDistrict(); const locationsByDistrict = await this.getLocationsByDistrict();
@ -111,9 +178,8 @@ export class PatrolUnitsSeeder {
for (let i = 1; i <= patrolCount; i++) { for (let i = 1; i <= patrolCount; i++) {
// Select patrol type based on weighted distribution // Select patrol type based on weighted distribution
const patrolType = this.getWeightedRandomItem(unitTypeWeights) as string; const patrolType = this.getWeightedRandomItem(unitTypeWeights) as string;
const callsign = this.getRandomCallsign();
const patrolName = `${unit.name.replace('Polsek', 'Patroli').replace('Polres', 'Patroli')} ${patrolType.charAt(0).toUpperCase() + patrolType.slice(1) const patrolName = `${callsign}-${unit.code_unit}-${this.typeCodeMap[patrolType]}${i}`;
} ${i}`;
const radius = getPatrolRadius(patrolType); const radius = getPatrolRadius(patrolType);
const status = this.getWeightedRandomItem(weightedStatus) as string; const status = this.getWeightedRandomItem(weightedStatus) as string;
@ -132,7 +198,7 @@ export class PatrolUnitsSeeder {
continue; continue;
} }
const typeCode = typeCodeMap[patrolType] || "P"; const typeCode = this.typeCodeMap[patrolType] || "P";
const codeUnitLast2 = unit.code_unit.slice(-2); const codeUnitLast2 = unit.code_unit.slice(-2);
try { try {
@ -149,6 +215,10 @@ export class PatrolUnitsSeeder {
CRegex.PATROL_UNIT_ID_REGEX 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({ patrolUnits.push({
id: newId, id: newId,
unit_id: unit.code_unit, unit_id: unit.code_unit,
@ -157,6 +227,8 @@ export class PatrolUnitsSeeder {
type: patrolType, type: patrolType,
status: status, status: status,
radius: radius, radius: radius,
member_count: memberCount,
category: category
}); });
} catch (error) { } catch (error) {
console.error(`Error generating ID for patrol unit: ${error}`); console.error(`Error generating ID for patrol unit: ${error}`);
@ -218,7 +290,7 @@ export class PatrolUnitsSeeder {
where: { where: {
user_id: user.id user_id: user.id
} }
}); })
if (!event) { if (!event) {
try { try {