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
|
// 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;
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,94 +82,109 @@ 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 =
|
||||||
children: [
|
controller.selectedPatrolType.value == PatrolUnitType.car;
|
||||||
Expanded(
|
final isMotorcycleSelected =
|
||||||
child: InkWell(
|
controller.selectedPatrolType.value ==
|
||||||
onTap: () {
|
PatrolUnitType.motorcycle;
|
||||||
controller.setPatrolUnitType(PatrolUnitType.car);
|
|
||||||
},
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
return Row(
|
||||||
decoration: BoxDecoration(
|
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:
|
color:
|
||||||
controller.selectedPatrolType.value ==
|
isCarSelected
|
||||||
PatrolUnitType.car
|
? isDark
|
||||||
? TColors.primary.withOpacity(0.1)
|
? TColors.accent
|
||||||
: Colors.transparent,
|
: TColors.primary
|
||||||
borderRadius: BorderRadius.circular(8),
|
: TColors.accent.withOpacity(0.3),
|
||||||
border: Border.all(
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
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,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: TSizes.sm),
|
||||||
child: Column(
|
const Text('Car'),
|
||||||
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(width: TSizes.spaceBtwInputFields),
|
),
|
||||||
Expanded(
|
const SizedBox(width: TSizes.spaceBtwInputFields),
|
||||||
child: InkWell(
|
Expanded(
|
||||||
onTap: () {
|
child: InkWell(
|
||||||
controller.setPatrolUnitType(
|
onTap: () {
|
||||||
PatrolUnitType.motorcycle,
|
controller.setPatrolUnitType(
|
||||||
);
|
PatrolUnitType.motorcycle,
|
||||||
},
|
);
|
||||||
child: Container(
|
},
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
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:
|
color:
|
||||||
controller.selectedPatrolType.value ==
|
isMotorcycleSelected
|
||||||
PatrolUnitType.motorcycle
|
? isDark
|
||||||
? TColors.primary.withOpacity(0.1)
|
? TColors.accent
|
||||||
: Colors.transparent,
|
: TColors.primary
|
||||||
borderRadius: BorderRadius.circular(8),
|
: TColors.accent.withOpacity(0.3),
|
||||||
border: Border.all(
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
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,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: TSizes.sm),
|
||||||
child: Column(
|
const Text('Motorcycle'),
|
||||||
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.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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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...',
|
||||||
|
@ -226,7 +237,7 @@ class ImageUploader extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Error overlay
|
// Error overlay
|
||||||
if (errorMessage != null &&
|
if (errorMessage != null &&
|
||||||
errorMessage!.isNotEmpty &&
|
errorMessage!.isNotEmpty &&
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -11,17 +11,18 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model profiles {
|
model profiles {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
user_id String @unique @db.Uuid
|
user_id String @unique @db.Uuid
|
||||||
avatar String? @db.VarChar(355)
|
avatar String? @db.VarChar(355)
|
||||||
username String? @unique @db.VarChar(255)
|
username String? @unique @db.VarChar(255)
|
||||||
first_name String? @db.VarChar(255)
|
first_name String? @db.VarChar(255)
|
||||||
last_name String? @db.VarChar(255)
|
last_name String? @db.VarChar(255)
|
||||||
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)
|
||||||
users users @relation(fields: [user_id], references: [id])
|
birth_place String?
|
||||||
|
users users @relation(fields: [user_id], references: [id])
|
||||||
|
|
||||||
@@index([user_id])
|
@@index([user_id])
|
||||||
@@index([username])
|
@@index([username])
|
||||||
|
@ -321,17 +322,19 @@ model units {
|
||||||
}
|
}
|
||||||
|
|
||||||
model patrol_units {
|
model patrol_units {
|
||||||
unit_id String @db.VarChar(20)
|
unit_id String @db.VarChar(20)
|
||||||
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)
|
||||||
status String @db.VarChar(50)
|
category patrol_unit_category? @default(group)
|
||||||
radius Float
|
member_count Int? @default(0)
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
status String @db.VarChar(50)
|
||||||
id String @id @unique @db.VarChar(100)
|
radius Float
|
||||||
members officers[]
|
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||||
location locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
id String @id @unique @db.VarChar(100)
|
||||||
unit units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
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([unit_id], map: "idx_patrol_units_unit_id")
|
||||||
@@index([location_id], map: "idx_patrol_units_location_id")
|
@@index([location_id], map: "idx_patrol_units_location_id")
|
||||||
|
@ -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
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { GeoJSONSeeder } from './seeds/geographic';
|
||||||
import { execSync } from 'child_process';
|
import { execSync } from 'child_process';
|
||||||
import { DemographicsSeeder } from './seeds/demographic';
|
import { DemographicsSeeder } from './seeds/demographic';
|
||||||
import { CrimeCategoriesSeeder } from './seeds/crime-category';
|
import { CrimeCategoriesSeeder } from './seeds/crime-category';
|
||||||
|
|
||||||
import { UnitSeeder } from './seeds/units';
|
import { UnitSeeder } from './seeds/units';
|
||||||
import { PatrolUnitsSeeder } from './seeds/patrol-units';
|
import { PatrolUnitsSeeder } from './seeds/patrol-units';
|
||||||
import { OfficersSeeder } from './seeds/officers';
|
import { OfficersSeeder } from './seeds/officers';
|
||||||
|
@ -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),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue