diff --git a/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart b/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart index 963927a..9fdaac3 100644 --- a/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart +++ b/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart @@ -1411,7 +1411,7 @@ class AzureOCRService { // Check if KTP has all required fields bool isKtpValid(Map 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; diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/id-card-verification/id_card_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/id-card-verification/id_card_verification_step.dart index 233b5b1..23b3588 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/id-card-verification/id_card_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/id-card-verification/id_card_verification_step.dart @@ -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), diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/patrol_unit_selection_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/patrol_unit_selection_screen.dart index 591907b..4fbbd18 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/patrol_unit_selection_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/patrol_unit_selection_screen.dart @@ -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( - 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), ), ); diff --git a/sigap-mobile/lib/src/shared/widgets/image_upload/image_uploader.dart b/sigap-mobile/lib/src/shared/widgets/image_upload/image_uploader.dart index 15f982e..60a255e 100644 --- a/sigap-mobile/lib/src/shared/widgets/image_upload/image_uploader.dart +++ b/sigap-mobile/lib/src/shared/widgets/image_upload/image_uploader.dart @@ -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...', @@ -226,7 +237,7 @@ class ImageUploader extends StatelessWidget { ), ), ), - + // Error overlay if (errorMessage != null && errorMessage!.isNotEmpty && diff --git a/sigap-mobile/lib/src/utils/loaders/circular_loader.dart b/sigap-mobile/lib/src/utils/loaders/circular_loader.dart index 852e396..4b6628f 100644 --- a/sigap-mobile/lib/src/utils/loaders/circular_loader.dart +++ b/sigap-mobile/lib/src/utils/loaders/circular_loader.dart @@ -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( diff --git a/sigap-website/prisma/schema.prisma b/sigap-website/prisma/schema.prisma index fea1150..3af0dc8 100644 --- a/sigap-website/prisma/schema.prisma +++ b/sigap-website/prisma/schema.prisma @@ -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 diff --git a/sigap-website/prisma/seed.ts b/sigap-website/prisma/seed.ts index da7ee5d..6526260 100644 --- a/sigap-website/prisma/seed.ts +++ b/sigap-website/prisma/seed.ts @@ -7,7 +7,7 @@ import { GeoJSONSeeder } from './seeds/geographic'; import { execSync } from 'child_process'; import { DemographicsSeeder } from './seeds/demographic'; import { CrimeCategoriesSeeder } from './seeds/crime-category'; - + import { UnitSeeder } from './seeds/units'; import { PatrolUnitsSeeder } from './seeds/patrol-units'; import { OfficersSeeder } from './seeds/officers'; @@ -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), ]; } diff --git a/sigap-website/prisma/seeds/patrol-units.ts b/sigap-website/prisma/seeds/patrol-units.ts index 324f964..92c1b42 100644 --- a/sigap-website/prisma/seeds/patrol-units.ts +++ b/sigap-website/prisma/seeds/patrol-units.ts @@ -11,9 +11,85 @@ export class PatrolUnitsSeeder { private supabase = createClient() ) { } - async run(): Promise { + // 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 = { + 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 { 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 = { - 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 {