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 9fdaac3..9473ec9 100644 --- a/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart +++ b/sigap-mobile/lib/src/cores/services/azure_ocr_service.dart @@ -942,12 +942,12 @@ class AzureOCRService { // First check for "Jabatan:" pattern explicitly for (int i = 0; i < allLines.length; i++) { String line = allLines[i].toLowerCase(); - if (line.contains('jabatan')) { + if (line.contains('pangkat')) { if (line.contains(':')) { String position = line.split(':')[1].trim(); - extractedInfo['jabatan'] = _normalizeCase(position); + extractedInfo['pangkat'] = _normalizeCase(position); print( - 'Found position from "jabatan:" pattern: ${extractedInfo['jabatan']}', + 'Found position from "jabatan:" pattern: ${extractedInfo['pangkat']}', ); return; } @@ -955,9 +955,9 @@ class AzureOCRService { // Check next line if (i + 1 < allLines.length) { String nextLine = allLines[i + 1].trim(); - extractedInfo['jabatan'] = _normalizeCase(nextLine); + extractedInfo['pangkat'] = _normalizeCase(nextLine); print( - 'Found position from line after "jabatan": ${extractedInfo['jabatan']}', + 'Found position from line after "jabatan": ${extractedInfo['pangkat']}', ); return; } @@ -988,8 +988,8 @@ class AzureOCRService { // Check if the entire line is a position for (String position in commonPositions) { if (line == position) { - extractedInfo['jabatan'] = position; - print('Found exact position match: ${extractedInfo['jabatan']}'); + extractedInfo['pangkat'] = position; + print('Found exact position match: ${extractedInfo['pangkat']}'); return; } } @@ -998,8 +998,8 @@ class AzureOCRService { for (String position in commonPositions) { if (line.contains(position)) { // Extract just the position part (this is more complex for real cards) - extractedInfo['jabatan'] = position; - print('Found position in line: ${extractedInfo['jabatan']}'); + extractedInfo['pangkat'] = position; + print('Found position in line: ${extractedInfo['pangkat']}'); return; } } @@ -1008,7 +1008,7 @@ class AzureOCRService { // Special handling for the sample data provided if (allLines.length >= 4 && allLines[3].trim().toUpperCase() == 'BRIGADIR') { - extractedInfo['jabatan'] = 'BRIGADIR'; + extractedInfo['pangkat'] = 'BRIGADIR'; print('Found position (BRIGADIR) at line 3'); return; } @@ -1018,8 +1018,8 @@ class AzureOCRService { String line = allLines[i].trim().toUpperCase(); for (String position in commonPositions) { if (line == position || line.contains(position)) { - extractedInfo['jabatan'] = position; - print('Found position in full scan: ${extractedInfo['jabatan']}'); + extractedInfo['pangkat'] = position; + print('Found position in full scan: ${extractedInfo['pangkat']}'); return; } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin/signin_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin/signin_controller.dart index a79bd54..e3bfc6c 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin/signin_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signin/signin_controller.dart @@ -42,7 +42,7 @@ class SignInController extends GetxController { // Navigate to sign up screen void goToSignUp() { - Get.toNamed(AppRoutes.signupWithRole); + Get.toNamed(AppRoutes.roleSelection); } // Clear error messages diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart index 9cceec8..7cbfbfa 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart @@ -41,15 +41,23 @@ class FormRegistrationController extends GetxController { late final OfficerInfoController? officerInfoController; late final UnitInfoController? unitInfoController; - late GlobalKey formKey; + // Current step in the registration process + final RxInt currentStep = 0.obs; + + // Total steps based on role + int get totalSteps => + selectedRole.value?.isOfficer ?? false + ? TNum + .totalStepOfficer // 3 steps for officers + : TNum.totalStepViewer; // 4 steps for viewers final storage = GetStorage(); - // Current step index - final RxInt currentStep = 0.obs; + // Loading state for form operations + final RxBool isLoading = false.obs; - // Total number of steps (depends on role) - late final int totalSteps; + // Form key for validation + GlobalKey? formKey; // User metadata model (kept for backward compatibility) final Rx userMetadata = UserMetadataModel().obs; @@ -61,9 +69,6 @@ class FormRegistrationController extends GetxController { // Officer data (kept for backward compatibility) final Rx officerModel = Rx(null); - // Loading state - final RxBool isLoading = false.obs; - // Form submission states final RxBool isSubmitting = RxBool(false); final RxString submitMessage = RxString(''); @@ -308,11 +313,9 @@ class FormRegistrationController extends GetxController { if (isOfficer) { officerInfoController = Get.find(); unitInfoController = Get.find(); - totalSteps = TNum.totalStepOfficer; } else { officerInfoController = null; unitInfoController = null; - totalSteps = TNum.totalStepViewer; } } @@ -562,196 +565,59 @@ class FormRegistrationController extends GetxController { // Get step titles based on role List getStepTitles() { if (selectedRole.value?.isOfficer ?? false) { - return ['Personal', 'ID Card', 'Selfie', 'Officer Info', 'Unit Info']; + // Officer steps - no personal info + return ['ID Card', 'Selfie', 'Officer Info']; } else { - return ['Personal', 'ID Card', 'Selfie', 'Identity']; + // Viewer steps - includes personal info + return ['Personal Info', 'ID Card', 'Selfie', 'Verify']; } } - // Get registration data for a specific step - T? getStepData() { - switch (T) { - case PersonalInfoData: - return registrationData.value.personalInfo as T?; - case IdCardVerificationData: - return registrationData.value.idCardVerification as T?; - case SelfieVerificationData: - return registrationData.value.selfieVerification as T?; - case IdentityVerificationData: - return registrationData.value.identityVerification as T?; - case OfficerInfoData: - return registrationData.value.officerInfo as T?; - case UnitInfoData: - return registrationData.value.unitInfo as T?; - default: - return null; - } - } - - // Update specific step data - void updateStepData(T data) { - switch (T) { - case PersonalInfoData: - registrationData.value = registrationData.value.copyWith( - personalInfo: data as PersonalInfoData, - ); - break; - case IdCardVerificationData: - registrationData.value = registrationData.value.copyWith( - idCardVerification: data as IdCardVerificationData, - ); - break; - case SelfieVerificationData: - registrationData.value = registrationData.value.copyWith( - selfieVerification: data as SelfieVerificationData, - ); - break; - case IdentityVerificationData: - registrationData.value = registrationData.value.copyWith( - identityVerification: data as IdentityVerificationData, - ); - break; - case OfficerInfoData: - registrationData.value = registrationData.value.copyWith( - officerInfo: data as OfficerInfoData, - ); - break; - case UnitInfoData: - registrationData.value = registrationData.value.copyWith( - unitInfo: data as UnitInfoData, - ); - break; - } - } - - // Validate current step - bool validateCurrentStep() { - switch (currentStep.value) { - case 0: - return personalInfoController.validate(formKey); - case 1: - return idCardVerificationController.validate(); - case 2: - return selfieVerificationController.isMatchWithIDCard.value; - case 3: - return selectedRole.value?.isOfficer == true - ? officerInfoController!.validate(formKey) - : identityController.validate(formKey); - case 4: - return selectedRole.value?.isOfficer == true - ? unitInfoController!.validate(formKey) - : true; - default: - return true; - } - } - - // Go to next step - void nextStep() { - // Special case for step 1 (ID Card step) - if (currentStep.value == 1) { - // Log step status - Logger().d( - 'ID Card step: confirmStatus=${idCardVerificationController.hasConfirmedIdCard.value}', - ); - - // Ensure ID card is confirmed before allowing to proceed - if (!idCardVerificationController.hasConfirmedIdCard.value) { - // Show a message that user needs to confirm the ID card first - TLoaders.errorSnackBar( - title: 'Action Required', - message: 'Please confirm your ID card image before proceeding.', - ); - return; + // Navigate to the next step + void nextStep() async { + // Validate the current form first + if (formKey?.currentState?.validate() ?? false) { + // If this is the last step, submit the form + if (currentStep.value == totalSteps - 1) { + await submitRegistration(); + } else { + // Otherwise, go to the next step + currentStep.value++; } - - // Pass data and proceed - // passIdCardDataToNextStep(); - currentStep.value++; // Directly increment step - return; - } - // Special case for step 2 (Selfie Verification step) - else if (currentStep.value == 2) { - // Log step status - Logger().d( - 'Selfie step: confirmStatus=${selfieVerificationController.hasConfirmedSelfie.value}', - ); - - // Ensure selfie is confirmed before allowing to proceed - if (!selfieVerificationController.hasConfirmedSelfie.value) { - // Show a message that user needs to confirm the selfie first - TLoaders.errorSnackBar( - title: 'Action Required', - message: 'Please confirm your selfie image before proceeding.', - ); - return; - } - - // Proceed to next step - currentStep.value++; // Directly increment step - return; - } - - // For all other steps, perform standard validation - if (!validateCurrentStep()) return; - - // Proceed to next step - if (currentStep.value < totalSteps - 1) { - // Fixed missing parenthesis - currentStep.value++; - } else { - // submitForm(); } } - void clearPreviousStepErrors() { - switch (currentStep.value) { - case 0: - personalInfoController.clearErrors(); - break; - case 1: - idCardVerificationController.clearErrors(); - break; - case 2: - selfieVerificationController.clearErrors(); - break; - case 3: - if (selectedRole.value?.isOfficer == true) { - officerInfoController?.clearErrors(); - } else { - identityController.clearErrors(); - } - break; - } - } - - // Go to previous step + // Navigate to the previous step void previousStep() { if (currentStep.value > 0) { - // Clear previous step errors - clearPreviousStepErrors(); - - // Decrement step currentStep.value--; } } - // Go to specific step + // Go to a specific step void goToStep(int step) { if (step >= 0 && step < totalSteps) { - // Only allow going to a step if all previous steps are valid - bool canProceed = true; - for (int i = 0; i < step; i++) { - currentStep.value = i; - if (!validateCurrentStep()) { - canProceed = false; - break; - } - } + currentStep.value = step; + } + } - if (canProceed) { - currentStep.value = step; - } + // Submit registration data + Future submitRegistration() async { + isLoading.value = true; + + try { + // Your registration submission logic here + await Future.delayed(const Duration(seconds: 2)); // Simulate API call + + // Handle successful registration + isLoading.value = false; + // Navigate to success page or home page + + return true; + } catch (e) { + isLoading.value = false; + // Handle error + return false; } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart index e09cec2..205d6e6 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart @@ -409,17 +409,7 @@ class IdCardVerificationController extends GetxController { if (isIdCardValid.value) { hasConfirmedIdCard.value = true; - // Log storage data for debugging - SharedPreferences.getInstance().then((prefs) { - Logger().i('Storage check on confirmation:'); - Logger().i( - 'OCR results: ${prefs.getString(_kOcrResultsKey)?.substring(0, 50)}...', - ); - Logger().i( - 'OCR model: ${prefs.getString(_kOcrModelKey)?.substring(0, 50)}...', - ); - Logger().i('ID card type: ${prefs.getString(_kIdCardTypeKey)}'); - }); + clearErrors(); // Clear any previous errors } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart index 3102736..19c0a1b 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart @@ -62,16 +62,37 @@ class OfficerInfoController extends GetxController { Rx selectedValidUntil = Rx(null); Rx selectedDateOfBirth = Rx(null); + // Dark mode reactive state + final Rx isDarkMode = false.obs; + + final isDark = Get.isDarkMode; + @override void onInit() { super.onInit(); + // Initialize isDarkMode with current value + _updateThemeMode(); + initRepositories(); // Fetch units after ensuring repositories are set up getAvailableUnits(); } + // Update theme mode based on current Get.isDarkMode value + void _updateThemeMode() { + // Check the brightness of the current theme + final brightness = Get.theme.brightness; + isDarkMode.value = brightness == Brightness.dark; + } + + // Method to check dark mode that can be called from UI + bool checkIsDarkMode(BuildContext context) { + final brightness = Theme.of(context).brightness; + return brightness == Brightness.dark; + } + void initRepositories() { // Check if repositories are already registered with GetX unitRepository = Get.find(); @@ -172,6 +193,48 @@ class OfficerInfoController extends GetxController { isValid = false; } + if (rankController.text.isEmpty) { + rankError.value = 'Rank is required'; + isValid = false; + } + + if (positionController.text.isEmpty) { + positionError.value = 'Position is required'; + isValid = false; + } + + if (phoneController.text.isEmpty) { + phoneError.value = 'Phone number is required'; + isValid = false; + } else if (!RegExp(r'^\+?[0-9]{10,15}$').hasMatch(phoneController.text)) { + phoneError.value = 'Invalid phone number format'; + isValid = false; + } + + // Date of Birth validation + if (dateOfBirthController.text.isEmpty) { + dateOfBirthError.value = 'Date of birth is required'; + isValid = false; + } else if (selectedDateOfBirth.value == null) { + dateOfBirthError.value = 'Please select a valid birth date'; + isValid = false; + } + + // Valid Until validation + if (validUntilController.text.isEmpty) { + validUntilError.value = 'Valid until date is required'; + isValid = false; + } else if (selectedValidUntil.value == null) { + validUntilError.value = 'Please select a valid date'; + isValid = false; + } + + if (placeOfBirthController.text.isEmpty) { + placeOfBirthError.value = 'Place of birth is required'; + isValid = false; + } + + // Unit selection validation - fixed logic if (unitIdController.text.isEmpty) { unitIdError.value = 'Please select a unit'; isValid = false; @@ -181,8 +244,21 @@ class OfficerInfoController extends GetxController { return isValid; } - void updateOfficer(OfficerModel officer, UserMetadataModel metadata) async { + void submitRegistration( + OfficerInfoController controller, + // FormRegistrationController mainController, + GlobalKey formKey, + ) async { try { + // First validate the form before showing the loading dialog + if (!validate(formKey)) { + TLoaders.errorSnackBar( + title: 'Validation Error', + message: 'Please fix the errors in the form before submitting.', + ); + return; + } + TCircularFullScreenLoader.openLoadingDialog(); final isConnected = await NetworkManager.instance.isConnected(); @@ -195,49 +271,57 @@ class OfficerInfoController extends GetxController { return; } - // Validate the form before proceeding - if (!validate(null)) { + // No need to validate again, already done above + final userId = AuthenticationRepository.instance.authUser!.id; + + final roleId = + AuthenticationRepository.instance.authUser!.userMetadata!['role_id']; + + // Check if the user has a valid role + if (roleId == null || roleId.isEmpty) { TLoaders.errorSnackBar( - title: 'Validation Error', - message: 'Please fix the errors in the form before submitting.', + title: 'Role Error', + message: 'User does not have a valid role assigned.', ); TCircularFullScreenLoader.stopLoading(); return; } - final data = officer.copyWith( - nrp: nrpController.text, - name: nameController.text, - rank: rankController.text, - position: positionController.text, - phone: phoneController.text, - unitId: unitIdController.text, - validUntil: selectedValidUntil.value, - placeOfBirth: placeOfBirthController.text, - dateOfBirth: selectedDateOfBirth.value, + OfficerModel officer = await OfficerRepository.instance.getOfficerById( + userId, ); - Logger().i('Updating officer with data: ${data.toJson()}'); + // Convert Map to UserMetadataModel - + // Create a new OfficerModel instance with the provided data + final updateOfficer = officer.copyWith( + unitId: unitIdController.text.trim(), + nrp: nrpController.text.trim(), + name: nameController.text.trim(), + rank: rankController.text.trim(), + position: positionController.text.trim(), + phone: phoneController.text.trim(), + placeOfBirth: placeOfBirthController.text.trim(), + dateOfBirth: selectedDateOfBirth.value, + validUntil: selectedValidUntil.value, + ); - // final updatedOfficer = await OfficerRepository.instance.updateOfficer( - // data, - // ); + // Logger().i('Updating officer with data: ${updateOfficer.toJson()}'); - // if (updatedOfficer == null) { - // TLoaders.errorSnackBar( - // title: 'Update Failed', - // message: 'Failed to update officer information. Please try again.', - // ); - // TCircularFullScreenLoader.stopLoading(); - // return; - // } + final updatedOfficer = await OfficerRepository.instance.updateOfficer( + updateOfficer, + ); - // final userMetadata = - // metadata.copyWith(profileStatus: 'completed').toAuthMetadataJson(); + if (updatedOfficer == null) { + TLoaders.errorSnackBar( + title: 'Update Failed', + message: 'Failed to update officer information. Please try again.', + ); + TCircularFullScreenLoader.stopLoading(); + return; + } - // await UserRepository.instance.updateUserMetadata(userMetadata); + await UserRepository.instance.updateProfileStatus('completed'); // TLoaders.successSnackBar( // title: 'Update Successful', @@ -245,18 +329,22 @@ class OfficerInfoController extends GetxController { // ); // resetForm(); - // TCircularFullScreenLoader.stopLoading(); + TCircularFullScreenLoader.stopLoading(); - // Get.off( - // () => StateScreen( - // title: 'Officer Information Created', - // subtitle: 'Officer information has been successfully create.', - // primaryButtonTitle: 'Back to signin', - // image: TImages.womanHuggingEarth, - // showButton: true, - // onPressed: () => AuthenticationRepository.instance.screenRedirect(), - // ), - // ); + Get.off( + () => StateScreen( + title: 'Officer Information Created', + subtitle: 'Officer information has been successfully created.', + primaryButtonTitle: 'Back to signin', + image: + isDarkMode.value + ? TImages.womanHuggingEarthDark + : TImages.womanHuggingEarth, + isSvg: true, + showButton: true, + onPressed: () => AuthenticationRepository.instance.screenRedirect(), + ), + ); } catch (e) { logger.e('Error updating officer: $e'); TCircularFullScreenLoader.stopLoading(); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart index 651a9e0..b6e7545 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart @@ -7,7 +7,6 @@ import 'package:sigap/src/features/auth/presentasion/pages/signup/step/identity- import 'package:sigap/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/step/personal-information/personal_info_step.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart'; -import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; import 'package:sigap/src/shared/widgets/indicators/step_indicator/step_indicator.dart'; import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; @@ -67,9 +66,6 @@ class FormRegistrationScreen extends StatelessWidget { ), ), ), - - // Navigation buttons - _buildNavigationButtons(controller), ], ), ); @@ -108,66 +104,36 @@ class FormRegistrationScreen extends StatelessWidget { ); } - Widget _buildNavigationButtons(FormRegistrationController controller) { - return Padding( - padding: const EdgeInsets.all(TSizes.defaultSpace), - child: Row( - children: [ - // Back button - Obx( - () => - controller.currentStep.value > 0 - ? Expanded( - child: Padding( - padding: const EdgeInsets.only(right: TSizes.sm), - child: AuthButton( - text: 'Previous', - onPressed: controller.previousStep, - ), - ), - ) - : const SizedBox.shrink(), - ), - - // Next/Submit button - Expanded( - child: Padding( - padding: EdgeInsets.only( - left: controller.currentStep.value > 0 ? TSizes.sm : 0.0, - ), - child: Obx( - () => AuthButton( - text: - controller.currentStep.value == controller.totalSteps - 1 - ? 'Submit' - : 'Next', - onPressed: controller.nextStep, - isLoading: controller.isLoading.value, - ), - ), - ), - ), - ], - ), - ); - } - Widget _buildStepContent(FormRegistrationController controller) { final isOfficer = controller.selectedRole.value?.isOfficer ?? false; - - switch (controller.currentStep.value) { - case 0: - return const PersonalInfoStep(); - case 1: - return const IdCardVerificationStep(); - case 2: - return const SelfieVerificationStep(); - case 3: - return isOfficer - ? const OfficerInfoStep() - : const IdentityVerificationStep(); - default: - return const SizedBox.shrink(); + + // Different step content for officer vs viewer + if (isOfficer) { + // Officer registration flow (3 steps) + switch (controller.currentStep.value) { + case 0: + return const IdCardVerificationStep(); + case 1: + return const SelfieVerificationStep(); + case 2: + return const OfficerInfoStep(); + default: + return const SizedBox.shrink(); + } + } else { + // Viewer registration flow (4 steps) + switch (controller.currentStep.value) { + case 0: + return const PersonalInfoStep(); + case 1: + return const IdCardVerificationStep(); + case 2: + return const SelfieVerificationStep(); + case 3: + return const IdentityVerificationStep(); + default: + return const SizedBox.shrink(); + } } } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart index b7e29bf..28595b7 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart @@ -7,7 +7,6 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart'; import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart'; -import 'package:sigap/src/features/onboarding/presentasion/controllers/role_selection_controller.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/image_strings.dart'; 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 23b3588..5116708 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 @@ -8,6 +8,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id- import 'package:sigap/src/shared/widgets/image_upload/image_source_dialog.dart'; import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart'; import 'package:sigap/src/shared/widgets/info/tips_container.dart'; +import 'package:sigap/src/shared/widgets/navigation/step_navigation_buttons.dart'; import 'package:sigap/src/shared/widgets/verification/ocr_result_card.dart'; import 'package:sigap/src/shared/widgets/verification/validation_message_card.dart'; import 'package:sigap/src/utils/constants/colors.dart'; @@ -146,11 +147,50 @@ class IdCardVerificationStep extends StatelessWidget { // Tips Section const SizedBox(height: TSizes.spaceBtwItems), _buildIdCardTips(idCardType), + + // Add space before navigation buttons + const SizedBox(height: TSizes.spaceBtwSections), + + // Navigation buttons with loading state + Obx( + () => StepNavigationButtons( + showPrevious: mainController.currentStep.value > 0, + isLastStep: false, + onPrevious: mainController.previousStep, + onNext: () => _handleNextStep(controller, mainController), + isLoading: + controller.isVerifying.value || + controller.isUploadingIdCard.value, + errorMessage: controller.idCardError.value, + nextButtonText: 'Continue', + ), + ), ], ), ); } + + // Add a method to handle the next step validation + void _handleNextStep( + IdCardVerificationController controller, + FormRegistrationController mainController, + ) { + // Validate that ID card is uploaded and verified + if (controller.idCardImage.value == null) { + controller.idCardError.value = 'Please upload your ID card to continue'; + return; + } + if (!controller.hasConfirmedIdCard.value) { + controller.idCardError.value = + 'Your ID card must be confirmed before going to the next step'; + return; + } + + // If everything is valid, go to next step + mainController.nextStep(); + } + Widget _buildHeader(BuildContext context, String idCardType) { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart index 3c95650..cea470f 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart @@ -5,6 +5,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/reg import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart'; import 'package:sigap/src/shared/widgets/form/form_section_header.dart'; +import 'package:sigap/src/shared/widgets/navigation/step_navigation_buttons.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/sizes.dart'; @@ -20,7 +21,9 @@ class OfficerInfoStep extends StatelessWidget { final mainController = Get.find(); mainController.formKey = formKey; - // Check if KTA data exists and populate fields if available + final isSubmitting = false.obs; + final submissionError = ''.obs; + _populateFieldsFromKta( controller, mainController.idCardVerificationController, @@ -53,7 +56,6 @@ class OfficerInfoStep extends StatelessWidget { controller.nrpError.value = ''; }, ), - _buildErrorText(controller.nrpError), // Name field CustomTextField( @@ -74,7 +76,6 @@ class OfficerInfoStep extends StatelessWidget { controller.nameError.value = ''; }, ), - _buildErrorText(controller.nameError), // Rank field CustomTextField( @@ -89,10 +90,10 @@ class OfficerInfoStep extends StatelessWidget { controller.rankError.value = ''; }, ), - _buildErrorText(controller.rankError), // Position field CustomTextField( + label: 'Position', controller: controller.positionController, validator: (v) => TValidators.validateUserInput('Position', v, 100), @@ -103,8 +104,8 @@ class OfficerInfoStep extends StatelessWidget { controller.positionController.text = value; controller.positionError.value = ''; }, + ), - _buildErrorText(controller.positionError), // Phone field CustomTextField( @@ -120,7 +121,6 @@ class OfficerInfoStep extends StatelessWidget { controller.phoneError.value = ''; }, ), - _buildErrorText(controller.phoneError), // Place of Birth field CustomTextField( @@ -136,41 +136,30 @@ class OfficerInfoStep extends StatelessWidget { controller.placeOfBirthError.value = ''; }, ), - _buildErrorText(controller.placeOfBirthError), - // Date of Birth field + // Date of Birth field _buildDateField( context: context, controller: controller, label: 'Date of Birth', textController: controller.dateOfBirthController, errorValue: controller.dateOfBirthError, - hintText: 'YYYY-MM-DD', - initialDate: DateTime.now().subtract( - const Duration(days: 365 * 18), - ), // Default to 18 years ago - firstDate: DateTime(1950), - lastDate: DateTime.now(), + hintText: 'Select your birth date', + dateType: DateFieldType.birthDate, onDateSelected: controller.setDateOfBirth, ), - _buildErrorText(controller.dateOfBirthError), - // Valid Until field + // Valid Until field _buildDateField( context: context, controller: controller, label: 'Valid Until', textController: controller.validUntilController, errorValue: controller.validUntilError, - hintText: 'YYYY-MM-DD', - initialDate: DateTime.now().add( - const Duration(days: 365), - ), // Default to 1 year from now - firstDate: DateTime.now(), - lastDate: DateTime(2100), + hintText: 'Select expiry date', + dateType: DateFieldType.validUntil, onDateSelected: controller.setValidUntilDate, ), - _buildErrorText(controller.validUntilError), const SizedBox(height: TSizes.spaceBtwSections), @@ -182,8 +171,9 @@ class OfficerInfoStep extends StatelessWidget { const SizedBox(height: TSizes.spaceBtwItems), - // Unit dropdown + // Unit dropdown with error styling _buildUnitDropdown( + context, controller, mainController.idCardVerificationController, ), @@ -225,41 +215,66 @@ class OfficerInfoStep extends StatelessWidget { ], ), ), + + const SizedBox(height: TSizes.spaceBtwSections), + + // Navigation buttons + Obx( + () => StepNavigationButtons( + showPrevious: true, + isLastStep: true, + onPrevious: mainController.previousStep, + onNext: () => controller.submitRegistration(controller, formKey), + isLoading: isSubmitting.value, + errorMessage: submissionError.value, + nextButtonText: 'Submit Registration', + ), + ), ], ), ); } - /// Populates form fields from KTA data if available void _populateFieldsFromKta( OfficerInfoController controller, IdCardVerificationController mainController, ) { - // Check if KTA data exists in the main controller final KtaModel? ktaData = mainController.ktaModel.value; - if (ktaData != null) { controller.populateFromKta(ktaData); } } - // Helper to build error text consistently + // error text with better styling Widget _buildErrorText(RxString errorValue) { return Obx( () => errorValue.value.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - errorValue.value, - style: TextStyle(color: Colors.red[700], fontSize: 12), + ? Container( + margin: const EdgeInsets.only(top: 6, left: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.error_outline, size: 14, color: TColors.error), + const SizedBox(width: 6), + Expanded( + child: Text( + errorValue.value, + style: TextStyle( + color: TColors.error, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), ) : const SizedBox.shrink(), ); } - // Build date picker field + // date picker with proper date ranges and error styling Widget _buildDateField({ required BuildContext context, required OfficerInfoController controller, @@ -267,36 +282,85 @@ class OfficerInfoStep extends StatelessWidget { required TextEditingController textController, required RxString errorValue, required String hintText, - required DateTime initialDate, - required DateTime firstDate, - required DateTime lastDate, + required DateFieldType dateType, required Function(DateTime) onDateSelected, }) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomTextField( - label: label, - controller: textController, - readOnly: true, // Make read-only since we use date picker - errorText: errorValue.value, - hintText: hintText, - suffixIcon: IconButton( - icon: const Icon(Icons.calendar_today), - onPressed: () async { + return Obx(() { + final hasError = errorValue.value.isNotEmpty; + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + + // Define date ranges based on type + DateTime initialDate; + DateTime firstDate; + DateTime lastDate; + + switch (dateType) { + case DateFieldType.birthDate: + // For birth date: past dates only + final now = DateTime.now(); + // Set initialDate to 25 years ago, allow dates from 1940 up to today (no future dates) + initialDate = DateTime( + now.year - 25, + now.month, + now.day, + ); // Default to 25 years ago + firstDate = DateTime(1940); // Allow very old dates + lastDate = DateTime( + now.year, + now.month, + now.day, + ); // Today (no future) + break; + case DateFieldType.validUntil: + // For valid until: future dates only + final now = DateTime.now(); + initialDate = DateTime( + now.year + 1, + now.month, + now.day, + ); // Default to 1 year from now + firstDate = now; // Start from today + lastDate = DateTime(2100); // Far future + break; + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Label + Text( + label, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: hasError ? TColors.error : null, + ), + ), + const SizedBox(height: TSizes.sm), + + // Date field container + GestureDetector( + onTap: () async { final date = await showDatePicker( context: context, - initialDate: initialDate, + initialDate: + textController.text.isNotEmpty + ? DateTime.tryParse(textController.text) ?? initialDate + : initialDate, firstDate: firstDate, lastDate: lastDate, + helpText: + dateType == DateFieldType.birthDate + ? 'Select your birth date' + : 'Select expiry date', builder: (context, child) { return Theme( - data: Theme.of(context).copyWith( - colorScheme: ColorScheme.light( - primary: Theme.of(context).primaryColor, - onPrimary: Colors.white, - surface: Colors.white, - onSurface: Colors.black, + data: theme.copyWith( + colorScheme: theme.colorScheme.copyWith( + primary: isDark ? TColors.accent : TColors.primary, + onPrimary: isDark ? TColors.primary : TColors.accent, + surface: isDark ? Colors.grey[800] : TColors.accent, + onSurface: isDark ? TColors.accent : Colors.black, ), ), child: child!, @@ -306,360 +370,474 @@ class OfficerInfoStep extends StatelessWidget { if (date != null) { onDateSelected(date); + errorValue.value = ''; // Clear error when date is selected } }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: TSizes.md, + vertical: TSizes.md, + ), + decoration: BoxDecoration( + border: Border.all( + color: + hasError + ? TColors.error + : (isDark ? Colors.grey[600]! : Colors.grey[300]!), + width: hasError ? 2 : 1, + ), + borderRadius: BorderRadius.circular(TSizes.inputFieldRadius), + color: (isDark ? TColors.dark : TColors.lightContainer), + ), + child: Row( + children: [ + Icon( + Icons.calendar_today, + color: + hasError + ? TColors.error + : (isDark ? Colors.grey[400] : Colors.grey[600]), + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + textController.text.isNotEmpty + ? _formatDisplayDate(textController.text) + : hintText, + style: theme.textTheme.bodyMedium?.copyWith( + color: + (hasError + ? TColors.error + : theme.textTheme.bodyMedium?.color), + ), + ), + ), + Icon( + Icons.keyboard_arrow_down, + color: + hasError + ? TColors.error + : (isDark ? Colors.grey[400] : Colors.grey[600]), + ), + ], + ), + ), ), - ), - ], - ); + + // Error message + _buildErrorText(errorValue), + + // Helper text for date range + if (!hasError && textController.text.isEmpty) + Container( + margin: const EdgeInsets.only(top: 6, left: 4), + child: Text( + dateType == DateFieldType.birthDate + ? 'Select a date from the past' + : 'Select a future date for document expiry', + style: TextStyle( + color: isDark ? Colors.grey[400] : Colors.grey[600], + fontSize: 11, + ), + ), + ), + + const SizedBox(height: TSizes.spaceBtwInputFields), + ], + ); + }); } - // Build unit dropdown selection + // unit dropdown with error styling Widget _buildUnitDropdown( + BuildContext context, OfficerInfoController controller, IdCardVerificationController idCardController, ) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Label - using context directly - Builder( - builder: (context) { - return Text( - 'Select Unit:', - style: Theme.of( - context, - ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), - ); - }, - ), + return Obx(() { + final hasError = controller.unitIdError.value.isNotEmpty; + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final borderRadius = BorderRadius.circular(TSizes.inputFieldRadius); + final fillColor = isDark ? TColors.dark : TColors.accent; - const SizedBox(height: TSizes.sm), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Label + Text( + 'Select Unit:', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + color: hasError ? TColors.error : null, + ), + ), + const SizedBox(height: TSizes.sm), - // Dropdown using Builder to access current context (and theme) - Builder( - builder: (context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final theme = Theme.of(context); - - // Use custom text field styling for consistency - final borderRadius = BorderRadius.circular(TSizes.inputFieldRadius); - final fillColor = isDark ? TColors.dark : TColors.lightContainer; - - return GetX( - builder: (controller) { - if (controller.isLoadingUnits.value) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: TSizes.md, - vertical: TSizes.md, + // Loading state + if (controller.isLoadingUnits.value) + Container( + padding: const EdgeInsets.symmetric( + horizontal: TSizes.md, + vertical: TSizes.md, + ), + decoration: BoxDecoration( + border: Border.all(color: theme.dividerColor), + borderRadius: borderRadius, + color: fillColor, + ), + child: Center( + child: Padding( + padding: const EdgeInsets.all(TSizes.sm), + child: CircularProgressIndicator(color: theme.primaryColor), + ), + ), + ) + // No units available + else if (controller.availableUnits.isEmpty) + Container( + padding: const EdgeInsets.symmetric( + horizontal: TSizes.md, + vertical: TSizes.md, + ), + decoration: BoxDecoration( + border: Border.all( + color: hasError ? TColors.error : theme.dividerColor, + width: hasError ? 2 : 1, + ), + borderRadius: borderRadius, + color: fillColor, + ), + child: Row( + children: [ + Icon( + Icons.warning_outlined, + color: hasError ? TColors.error : Colors.orange[600], + size: 20, + ), + const SizedBox(width: 12), + Text( + 'No units available', + style: theme.textTheme.bodyMedium?.copyWith( + color: hasError ? TColors.error : Colors.orange[600], ), - decoration: BoxDecoration( - border: Border.all(color: theme.dividerColor), - borderRadius: borderRadius, - color: fillColor, - ), - child: Center( - child: Padding( - padding: const EdgeInsets.all(TSizes.sm), - child: CircularProgressIndicator( - color: theme.primaryColor, - ), - ), - ), - ); + ), + ], + ), + ) + // Units available + else + _buildUnitDropdownContent( + controller, + idCardController, + hasError, + theme, + isDark, + borderRadius, + fillColor, + ), + + // Error message + _buildErrorText(controller.unitIdError), + + const SizedBox(height: TSizes.spaceBtwInputFields), + ], + ); + }); + } + + Widget _buildUnitDropdownContent( + OfficerInfoController controller, + IdCardVerificationController idCardController, + bool hasError, + ThemeData theme, + bool isDark, + BorderRadius borderRadius, + Color fillColor, + ) { + return GetX( + builder: (controller) { + final selectedUnit = controller.availableUnits.firstWhereOrNull( + (unit) => unit.codeUnit == controller.unitIdController.text, + ); + + // Auto-select matching unit from KTA data + if (controller.availableUnits.isNotEmpty && + controller.unitIdController.text.isEmpty) { + final ktaUnit = idCardController.ktaModel.value?.policeUnit ?? ''; + final matchingUnit = controller.availableUnits.firstWhereOrNull( + (unit) => + unit.name.toLowerCase() == ktaUnit.toLowerCase() || + unit.name.toLowerCase().contains(ktaUnit.toLowerCase()) || + ktaUnit.toLowerCase().contains(unit.name.toLowerCase()), + ); + + if (matchingUnit != null) { + Future.microtask(() => controller.onUnitSelected(matchingUnit)); + } + } + + return Column( + children: [ + // Dropdown Selection Button + GestureDetector( + onTap: () { + controller.isUnitDropdownOpen.toggle(); + if (hasError) { + controller.unitIdError.value = + ''; // Clear error on interaction } - - if (controller.availableUnits.isEmpty) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: TSizes.md, - vertical: TSizes.md, - ), - decoration: BoxDecoration( - border: Border.all(color: theme.dividerColor), - borderRadius: borderRadius, - color: fillColor, - ), - child: Text( - 'No units available', - style: theme.textTheme.bodyMedium?.copyWith( - color: isDark ? Colors.grey[400] : Colors.grey[600], - ), - ), - ); - } - - // Get the selected unit (if any) - final selectedUnit = controller.availableUnits.firstWhereOrNull( - (unit) => unit.codeUnit == controller.unitIdController.text, - ); - - // If units are loaded and we have KTA data with a police unit, - // try to find a matching unit and select it - if (controller.availableUnits.isNotEmpty && - controller.unitIdController.text.isEmpty) { - final ktaUnit = - idCardController.ktaModel.value?.policeUnit ?? ''; - - // More flexible matching logic to find the best matching unit - final matchingUnit = controller.availableUnits.firstWhereOrNull( - (unit) => - // Try exact match first - unit.name.toLowerCase() == ktaUnit.toLowerCase() || - // Then try contains match - unit.name.toLowerCase().contains( - ktaUnit.toLowerCase(), - ) || - // Or if the KTA unit contains the available unit name - ktaUnit.toLowerCase().contains(unit.name.toLowerCase()), - ); - - if (matchingUnit != null) { - // Use Future.microtask to avoid setState during build - Future.microtask( - () => controller.onUnitSelected(matchingUnit), - ); - } - } - - return Column( + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: TSizes.md, + vertical: TSizes.md, + ), + decoration: BoxDecoration( + border: Border.all( + color: + hasError + ? TColors.error + : (controller.isUnitDropdownOpen.value + ? theme.primaryColor + : theme.dividerColor), + width: + hasError || controller.isUnitDropdownOpen.value ? 2 : 1, + ), + borderRadius: borderRadius, + color: fillColor, + ), + child: Row( children: [ - // Dropdown Selection Button - GestureDetector( - onTap: () { - // Toggle dropdown visibility - controller.isUnitDropdownOpen.toggle(); - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: TSizes.md, - vertical: TSizes.md, - ), - decoration: BoxDecoration( - border: Border.all( - color: - controller.isUnitDropdownOpen.value - ? theme.primaryColor - : theme.dividerColor, - width: - controller.isUnitDropdownOpen.value ? 1.5 : 1, - ), - borderRadius: borderRadius, - color: fillColor, - ), - child: Row( - children: [ - Expanded( - child: Text( - selectedUnit != null - ? '${selectedUnit.name} (${selectedUnit.type.name})' - : 'Select Unit', - style: theme.textTheme.bodyMedium?.copyWith( - color: - selectedUnit != null - ? theme.textTheme.bodyMedium?.color - : (isDark - ? Colors.grey[400] - : Colors.grey[600]), - ), - ), - ), - Icon( - controller.isUnitDropdownOpen.value - ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down, - color: - controller.isUnitDropdownOpen.value - ? theme.primaryColor + Icon( + selectedUnit != null + ? Icons.shield + : Icons.shield_outlined, + color: + hasError + ? TColors.error + : (selectedUnit != null + ? theme.primaryColor + : (isDark + ? Colors.grey[400] + : Colors.grey[600])), + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + selectedUnit != null + ? '${selectedUnit.name} (${selectedUnit.type.name})' + : 'Select Unit', + style: theme.textTheme.bodyMedium?.copyWith( + color: + hasError + ? TColors.error + : (selectedUnit != null + ? theme.textTheme.bodyMedium?.color : (isDark ? Colors.grey[400] - : Colors.grey[600]), - ), - ], + : Colors.grey[600])), ), ), ), + Icon( + controller.isUnitDropdownOpen.value + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + color: + hasError + ? TColors.error + : (controller.isUnitDropdownOpen.value + ? theme.primaryColor + : (isDark + ? Colors.grey[400] + : Colors.grey[600])), + ), + ], + ), + ), + ), - // Dropdown Options - if (controller.isUnitDropdownOpen.value) - Container( - margin: const EdgeInsets.only(top: 4), - decoration: BoxDecoration( - color: fillColor, - borderRadius: borderRadius, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity( - isDark ? 0.3 : 0.1, + // Dropdown Options + if (controller.isUnitDropdownOpen.value) + Container( + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: fillColor, + borderRadius: borderRadius, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(isDark ? 0.3 : 0.1), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + border: Border.all( + color: hasError ? TColors.error : theme.dividerColor, + ), + ), + constraints: const BoxConstraints(maxHeight: 250), + child: ClipRRect( + borderRadius: borderRadius, + child: ListView.builder( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: controller.availableUnits.length, + itemBuilder: (context, index) { + final unit = controller.availableUnits[index]; + final isSelected = + unit.codeUnit == controller.unitIdController.text; + + return GestureDetector( + onTap: () { + controller.onUnitSelected(unit); + controller.isUnitDropdownOpen.value = false; + controller.unitIdError.value = + ''; // Clear error on selection + }, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: TSizes.md, + vertical: TSizes.md - 2, + ), + decoration: BoxDecoration( + color: + isSelected + ? theme.primaryColor.withOpacity( + isDark ? 0.2 : 0.1, + ) + : Colors.transparent, + border: + index < controller.availableUnits.length - 1 + ? Border( + bottom: BorderSide( + color: theme.dividerColor.withOpacity( + 0.5, + ), + width: 0.5, + ), + ) + : null, + ), + child: Row( + children: [ + Icon( + isSelected + ? Icons.shield + : Icons.shield_outlined, + color: + isSelected + ? theme.primaryColor + : (isDark + ? Colors.grey[400] + : Colors.grey[600]), + size: 20, ), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - border: Border.all(color: theme.dividerColor), - ), - constraints: const BoxConstraints(maxHeight: 250), - child: ClipRRect( - borderRadius: borderRadius, - child: ListView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - itemCount: controller.availableUnits.length, - itemBuilder: (context, index) { - final unit = controller.availableUnits[index]; - final isSelected = - unit.codeUnit == - controller.unitIdController.text; - - return GestureDetector( - onTap: () { - controller.onUnitSelected(unit); - controller.isUnitDropdownOpen.value = false; - }, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: TSizes.md, - vertical: TSizes.md - 2, - ), - decoration: BoxDecoration( + const SizedBox(width: 12), + Expanded( + child: Text( + '${unit.name} (${unit.type.name})', + style: theme.textTheme.bodyMedium?.copyWith( color: isSelected - ? theme.primaryColor.withOpacity( - isDark ? 0.2 : 0.1, - ) - : Colors.transparent, - border: - index < - controller - .availableUnits - .length - - 1 - ? Border( - bottom: BorderSide( - color: theme.dividerColor - .withOpacity(0.5), - width: 0.5, - ), - ) - : null, - ), - child: Row( - children: [ - // Unit Icon - Icon( + ? theme.primaryColor + : theme.textTheme.bodyMedium?.color, + fontWeight: isSelected - ? Icons.shield - : Icons.shield_outlined, - color: - isSelected - ? theme.primaryColor - : (isDark - ? Colors.grey[400] - : Colors.grey[600]), - size: 20, - ), - const SizedBox(width: 12), - - // Unit Name - Expanded( - child: Text( - '${unit.name} (${unit.type.name})', - style: theme.textTheme.bodyMedium - ?.copyWith( - color: - isSelected - ? theme.primaryColor - : theme - .textTheme - .bodyMedium - ?.color, - fontWeight: - isSelected - ? FontWeight.bold - : FontWeight.normal, - ), - ), - ), - - // Checkmark for selected item - if (isSelected) - Icon( - Icons.check, - color: theme.primaryColor, - size: 18, - ), - ], + ? FontWeight.bold + : FontWeight.normal, ), ), - ); - }, - ), - ), - ), - - // Selected unit display - if (selectedUnit != null && - !controller.isUnitDropdownOpen.value) - Container( - margin: const EdgeInsets.only( - top: TSizes.spaceBtwInputFields, - ), - padding: const EdgeInsets.all(TSizes.md), - decoration: BoxDecoration( - color: theme.primaryColor.withOpacity( - isDark ? 0.2 : 0.1, - ), - borderRadius: borderRadius, - border: Border.all(color: theme.primaryColor), - ), - child: Row( - children: [ - Icon( - Icons.shield_outlined, - color: theme.primaryColor, - ), - const SizedBox(width: TSizes.sm), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Selected Unit', - style: theme.textTheme.labelSmall?.copyWith( - color: - isDark - ? Colors.grey[400] - : Colors.grey[600], - ), - ), - Text( - '${selectedUnit.name} (${selectedUnit.type.name})', - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - ), - ], ), - ), - Icon( - Icons.check_circle, - color: theme.primaryColor, - size: 20, - ), - ], + if (isSelected) + Icon( + Icons.check, + color: theme.primaryColor, + size: 18, + ), + ], + ), ), - ), + ); + }, + ), + ), + ), - // Error message - _buildErrorText(controller.unitIdError), + // Selected unit display + if (selectedUnit != null && !controller.isUnitDropdownOpen.value) + Container( + margin: const EdgeInsets.only(top: TSizes.spaceBtwInputFields), + padding: const EdgeInsets.all(TSizes.md), + decoration: BoxDecoration( + color: theme.primaryColor.withOpacity(isDark ? 0.2 : 0.1), + borderRadius: borderRadius, + border: Border.all(color: theme.primaryColor), + ), + child: Row( + children: [ + Icon(Icons.shield_outlined, color: theme.primaryColor), + const SizedBox(width: TSizes.sm), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Selected Unit', + style: theme.textTheme.labelSmall?.copyWith( + color: + isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + Text( + '${selectedUnit.name} (${selectedUnit.type.name})', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + Icon( + Icons.check_circle, + color: theme.primaryColor, + size: 20, + ), ], - ); - }, - ); - }, - ), - ], + ), + ), + ], + ); + }, ); } + + // Helper method to format date for display + String _formatDisplayDate(String dateString) { + try { + final date = DateTime.parse(dateString); + final months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + return '${date.day} ${months[date.month - 1]} ${date.year}'; + } catch (e) { + return dateString; + } + } } + +// Enum for date field types +enum DateFieldType { birthDate, validUntil } 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 9c57f96..7d5543c 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 @@ -1,1173 +1,1173 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart'; -import 'package:sigap/src/features/daily-ops/controllers/patrol_unit_controller.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/sizes.dart'; -import 'package:sigap/src/utils/helpers/helper_functions.dart'; -import 'package:sigap/src/utils/popups/loaders.dart'; +// import 'package:flutter/material.dart'; +// import 'package:get/get.dart'; +// import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart'; +// import 'package:sigap/src/features/daily-ops/controllers/patrol_unit_controller.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/sizes.dart'; +// import 'package:sigap/src/utils/helpers/helper_functions.dart'; +// import 'package:sigap/src/utils/popups/loaders.dart'; -class PatrolUnitSelectionScreen extends StatelessWidget { - PatrolUnitSelectionScreen({super.key}); +// class PatrolUnitSelectionScreen extends StatelessWidget { +// PatrolUnitSelectionScreen({super.key}); - // Use the dedicated patrol unit controller - final officerController = Get.find(); - final controller = Get.find(); +// // Use the dedicated patrol unit controller +// final officerController = Get.find(); +// final controller = Get.find(); - @override - Widget build(BuildContext context) { - final isDark = THelperFunctions.isDarkMode(context); - return Scaffold( - appBar: AppBar( - title: const Text('Configure Patrol Unit'), - leading: IconButton( - icon: Icon( - Icons.arrow_back, - color: isDark ? TColors.accent : TColors.primary, - ), - onPressed: () => Get.back(), - ), - centerTitle: true, - ), - body: Padding( - padding: const EdgeInsets.all(TSizes.defaultSpace), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Unit info display - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: - isDark - ? TColors.accent.withOpacity(0.1) - : TColors.primary.withOpacity(0.1), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Icon( - Icons.business, - color: isDark ? TColors.accent : TColors.primary, - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Selected Unit', - style: Theme.of( - context, - ).textTheme.labelSmall?.copyWith( - color: - isDark - ? TColors.accent.withOpacity(0.5) - : TColors.primary.withOpacity(0.5), - ), - ), - Obx( - () => Text( - controller.selectedUnitName.value, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ), - ], - ), - ), - ], - ), - ), +// @override +// Widget build(BuildContext context) { +// final isDark = THelperFunctions.isDarkMode(context); +// return Scaffold( +// appBar: AppBar( +// title: const Text('Configure Patrol Unit'), +// leading: IconButton( +// icon: Icon( +// Icons.arrow_back, +// color: isDark ? TColors.accent : TColors.primary, +// ), +// onPressed: () => Get.back(), +// ), +// centerTitle: true, +// ), +// body: Padding( +// padding: const EdgeInsets.all(TSizes.defaultSpace), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// // Unit info display +// Container( +// padding: const EdgeInsets.all(12), +// decoration: BoxDecoration( +// color: +// isDark +// ? TColors.accent.withOpacity(0.1) +// : TColors.primary.withOpacity(0.1), +// borderRadius: BorderRadius.circular(8), +// ), +// child: Row( +// children: [ +// Icon( +// Icons.business, +// color: isDark ? TColors.accent : TColors.primary, +// ), +// const SizedBox(width: 12), +// Expanded( +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// 'Selected Unit', +// style: Theme.of( +// context, +// ).textTheme.labelSmall?.copyWith( +// color: +// isDark +// ? TColors.accent.withOpacity(0.5) +// : TColors.primary.withOpacity(0.5), +// ), +// ), +// Obx( +// () => Text( +// controller.selectedUnitName.value, +// style: const TextStyle(fontWeight: FontWeight.bold), +// ), +// ), +// ], +// ), +// ), +// ], +// ), +// ), - const SizedBox(height: TSizes.spaceBtwSections), +// const SizedBox(height: TSizes.spaceBtwSections), - // Patrol Type Selection - const Text( - 'Patrol Type:', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: TSizes.spaceBtwInputFields / 2), - _buildPatrolTypeBasicSelection(), +// // Patrol Type Selection +// const Text( +// 'Patrol Type:', +// style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), +// ), +// const SizedBox(height: TSizes.spaceBtwInputFields / 2), +// _buildPatrolTypeBasicSelection(), - const SizedBox(height: TSizes.spaceBtwSections), +// const SizedBox(height: TSizes.spaceBtwSections), - // Selection Mode Tabs - const Text( - 'Select option:', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: TSizes.spaceBtwInputFields / 2), - _buildSelectionModeTabs(), +// // Selection Mode Tabs +// const Text( +// 'Select option:', +// style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), +// ), +// const SizedBox(height: TSizes.spaceBtwInputFields / 2), +// _buildSelectionModeTabs(), - const SizedBox(height: TSizes.spaceBtwInputFields), +// const SizedBox(height: TSizes.spaceBtwInputFields), - // Patrol Unit Selection/Creation based on mode - Expanded( - child: GetBuilder( - id: 'patrol_selection', - builder: (controller) { - switch (controller.patrolSelectionMode.value) { - case PatrolSelectionMode.individual: - case PatrolSelectionMode.group: - return _buildExistingPatrolUnitSelection(); - case PatrolSelectionMode.createNew: - return _buildCreatePatrolUnitForm(); - } - }, - ), - ), +// // Patrol Unit Selection/Creation based on mode +// Expanded( +// child: GetBuilder( +// id: 'patrol_selection', +// builder: (controller) { +// switch (controller.patrolSelectionMode.value) { +// case PatrolSelectionMode.individual: +// case PatrolSelectionMode.group: +// return _buildExistingPatrolUnitSelection(); +// case PatrolSelectionMode.createNew: +// return _buildCreatePatrolUnitForm(); +// } +// }, +// ), +// ), - // Confirm button - Padding( - padding: const EdgeInsets.symmetric( - vertical: TSizes.defaultSpace, - ), - child: SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: _handleConfirmButton, - style: ElevatedButton.styleFrom( - backgroundColor: TColors.primary, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - child: const Text('Confirm Selection'), - ), - ), - ), - ], - ), - ), - ); - } +// // Confirm button +// Padding( +// padding: const EdgeInsets.symmetric( +// vertical: TSizes.defaultSpace, +// ), +// child: SizedBox( +// width: double.infinity, +// child: ElevatedButton( +// onPressed: _handleConfirmButton, +// style: ElevatedButton.styleFrom( +// backgroundColor: TColors.primary, +// foregroundColor: Colors.white, +// padding: const EdgeInsets.symmetric(vertical: 16), +// ), +// child: const Text('Confirm Selection'), +// ), +// ), +// ), +// ], +// ), +// ), +// ); +// } - // Handle the confirm button press based on mode - void _handleConfirmButton() { - if (controller.patrolSelectionMode.value == PatrolSelectionMode.createNew) { - controller.createNewPatrolUnit().then((success) { - if (success) { - Get.back(); - } - }); - } else if (controller.patrolUnitIdController.text.isNotEmpty) { - // Selection mode - just go back if a patrol unit is selected - Get.back(); - } else { - TLoaders.errorSnackBar( - title: 'Patrol Unit Required', - message: 'Please select a patrol unit', - ); - } - } +// // Handle the confirm button press based on mode +// void _handleConfirmButton() { +// if (controller.patrolSelectionMode.value == PatrolSelectionMode.createNew) { +// controller.createNewPatrolUnit().then((success) { +// if (success) { +// Get.back(); +// } +// }); +// } else if (controller.patrolUnitIdController.text.isNotEmpty) { +// // Selection mode - just go back if a patrol unit is selected +// Get.back(); +// } else { +// TLoaders.errorSnackBar( +// title: 'Patrol Unit Required', +// message: 'Please select a patrol unit', +// ); +// } +// } - // Build patrol type selection (basic car/motorcycle selection) - Widget _buildPatrolTypeBasicSelection() { - return GetBuilder( - id: 'patrol_type_selector', - builder: (controller) { - final isDark = THelperFunctions.isDarkMode(Get.context!); - final isCarSelected = - controller.selectedPatrolType.value == PatrolUnitTypeExtended.car; - final isMotorcycleSelected = - controller.selectedPatrolType.value == - PatrolUnitTypeExtended.motorcycle; +// // Build patrol type selection (basic car/motorcycle selection) +// Widget _buildPatrolTypeBasicSelection() { +// return GetBuilder( +// id: 'patrol_type_selector', +// builder: (controller) { +// final isDark = THelperFunctions.isDarkMode(Get.context!); +// final isCarSelected = +// controller.selectedPatrolType.value == PatrolUnitTypeExtended.car; +// final isMotorcycleSelected = +// controller.selectedPatrolType.value == +// PatrolUnitTypeExtended.motorcycle; - return Row( - children: [ - Expanded( - child: InkWell( - onTap: - () => controller.setPatrolUnitType( - PatrolUnitTypeExtended.car, - ), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: - isCarSelected - ? isDark - ? TColors.accent.withOpacity(0.1) - : TColors.primary.withOpacity(0.1) - : isDark - ? TColors.accent.withOpacity(0.05) - : TColors.lightContainer.withOpacity(0.05), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: - isCarSelected - ? isDark - ? TColors.accent - : TColors.primary - : isDark - ? TColors.accent.withOpacity(0.3) - : TColors.primary.withOpacity(0.3), - ), - ), - child: Column( - children: [ - Icon( - Icons.directions_car, - color: - isCarSelected - ? isDark - ? TColors.accent - : TColors.primary - : isDark - ? TColors.accent.withOpacity(0.3) - : TColors.primary.withOpacity(0.3), - size: 32, - ), - const SizedBox(height: TSizes.sm), - const Text('Car'), - ], - ), - ), - ), - ), - const SizedBox(width: TSizes.spaceBtwInputFields), - Expanded( - child: InkWell( - onTap: - () => controller.setPatrolUnitType( - PatrolUnitTypeExtended.motorcycle, - ), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: - isMotorcycleSelected - ? isDark - ? TColors.accent.withOpacity(0.1) - : TColors.primary.withOpacity(0.1) - : isDark - ? TColors.accent.withOpacity(0.05) - : TColors.lightContainer.withOpacity(0.05), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: - isMotorcycleSelected - ? isDark - ? TColors.accent - : TColors.primary - : isDark - ? TColors.accent.withOpacity(0.3) - : TColors.primary.withOpacity(0.3), - ), - ), - child: Column( - children: [ - Icon( - Icons.motorcycle, - color: - isMotorcycleSelected - ? isDark - ? TColors.accent - : TColors.primary - : isDark - ? TColors.accent.withOpacity(0.3) - : TColors.primary.withOpacity(0.3), - size: 32, - ), - const SizedBox(height: TSizes.sm), - const Text('Motorcycle'), - ], - ), - ), - ), - ), - ], - ); - }, - ); - } +// return Row( +// children: [ +// Expanded( +// child: InkWell( +// onTap: +// () => controller.setPatrolUnitType( +// PatrolUnitTypeExtended.car, +// ), +// child: Container( +// padding: const EdgeInsets.symmetric(vertical: 12), +// decoration: BoxDecoration( +// color: +// isCarSelected +// ? isDark +// ? TColors.accent.withOpacity(0.1) +// : TColors.primary.withOpacity(0.1) +// : isDark +// ? TColors.accent.withOpacity(0.05) +// : TColors.lightContainer.withOpacity(0.05), +// borderRadius: BorderRadius.circular(8), +// border: Border.all( +// color: +// isCarSelected +// ? isDark +// ? TColors.accent +// : TColors.primary +// : isDark +// ? TColors.accent.withOpacity(0.3) +// : TColors.primary.withOpacity(0.3), +// ), +// ), +// child: Column( +// children: [ +// Icon( +// Icons.directions_car, +// color: +// isCarSelected +// ? isDark +// ? TColors.accent +// : TColors.primary +// : isDark +// ? TColors.accent.withOpacity(0.3) +// : TColors.primary.withOpacity(0.3), +// size: 32, +// ), +// const SizedBox(height: TSizes.sm), +// const Text('Car'), +// ], +// ), +// ), +// ), +// ), +// const SizedBox(width: TSizes.spaceBtwInputFields), +// Expanded( +// child: InkWell( +// onTap: +// () => controller.setPatrolUnitType( +// PatrolUnitTypeExtended.motorcycle, +// ), +// child: Container( +// padding: const EdgeInsets.symmetric(vertical: 12), +// decoration: BoxDecoration( +// color: +// isMotorcycleSelected +// ? isDark +// ? TColors.accent.withOpacity(0.1) +// : TColors.primary.withOpacity(0.1) +// : isDark +// ? TColors.accent.withOpacity(0.05) +// : TColors.lightContainer.withOpacity(0.05), +// borderRadius: BorderRadius.circular(8), +// border: Border.all( +// color: +// isMotorcycleSelected +// ? isDark +// ? TColors.accent +// : TColors.primary +// : isDark +// ? TColors.accent.withOpacity(0.3) +// : TColors.primary.withOpacity(0.3), +// ), +// ), +// child: Column( +// children: [ +// Icon( +// Icons.motorcycle, +// color: +// isMotorcycleSelected +// ? isDark +// ? TColors.accent +// : TColors.primary +// : isDark +// ? TColors.accent.withOpacity(0.3) +// : TColors.primary.withOpacity(0.3), +// size: 32, +// ), +// const SizedBox(height: TSizes.sm), +// const Text('Motorcycle'), +// ], +// ), +// ), +// ), +// ), +// ], +// ); +// }, +// ); +// } - // Build selection mode tabs (individual/group/create new) - Widget _buildSelectionModeTabs() { - return GetBuilder( - id: 'patrol_mode_tabs', - builder: (controller) { - return Row( - children: [ - _buildModeTab( - controller, - PatrolSelectionMode.individual, - 'Individual', - Icons.person, - ), - const SizedBox(width: TSizes.spaceBtwInputFields), - _buildModeTab( - controller, - PatrolSelectionMode.group, - 'Group', - Icons.group, - ), - const SizedBox(width: TSizes.spaceBtwInputFields), - _buildModeTab( - controller, - PatrolSelectionMode.createNew, - 'Create New', - Icons.add_circle, - ), - ], - ); - }, - ); - } +// // Build selection mode tabs (individual/group/create new) +// Widget _buildSelectionModeTabs() { +// return GetBuilder( +// id: 'patrol_mode_tabs', +// builder: (controller) { +// return Row( +// children: [ +// _buildModeTab( +// controller, +// PatrolSelectionMode.individual, +// 'Individual', +// Icons.person, +// ), +// const SizedBox(width: TSizes.spaceBtwInputFields), +// _buildModeTab( +// controller, +// PatrolSelectionMode.group, +// 'Group', +// Icons.group, +// ), +// const SizedBox(width: TSizes.spaceBtwInputFields), +// _buildModeTab( +// controller, +// PatrolSelectionMode.createNew, +// 'Create New', +// Icons.add_circle, +// ), +// ], +// ); +// }, +// ); +// } - // Build existing patrol unit selection list - Widget _buildExistingPatrolUnitSelection() { - final isDark = THelperFunctions.isDarkMode(Get.context!); +// // Build existing patrol unit selection list +// Widget _buildExistingPatrolUnitSelection() { +// final isDark = THelperFunctions.isDarkMode(Get.context!); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Category label based on selection mode - Text( - controller.patrolSelectionMode.value == PatrolSelectionMode.individual - ? 'Select Individual Patrol Unit:' - : 'Select Group Patrol Unit:', - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// // Category label based on selection mode +// Text( +// controller.patrolSelectionMode.value == PatrolSelectionMode.individual +// ? 'Select Individual Patrol Unit:' +// : 'Select Group Patrol Unit:', +// style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), +// ), - const SizedBox(height: TSizes.spaceBtwInputFields / 2), +// const SizedBox(height: TSizes.spaceBtwInputFields / 2), - GetX( - builder: (controller) { - if (controller.isLoadingPatrolUnits.value) { - return const Expanded( - child: Center(child: CircularProgressIndicator()), - ); - } +// GetX( +// builder: (controller) { +// if (controller.isLoadingPatrolUnits.value) { +// return const Expanded( +// child: Center(child: CircularProgressIndicator()), +// ); +// } - if (controller.unitIdController.text.isEmpty) { - return const Expanded( - child: Center(child: Text('Please select a unit first')), - ); - } +// if (controller.unitIdController.text.isEmpty) { +// return const Expanded( +// child: Center(child: Text('Please select a unit first')), +// ); +// } - final filteredUnits = controller.getFilteredPatrolUnits(); +// final filteredUnits = controller.getFilteredPatrolUnits(); - if (filteredUnits.isEmpty) { - // Show different messages based on the filter mode - String message = ''; - if (controller.patrolSelectionMode.value == - PatrolSelectionMode.individual) { - message = 'No individual patrol units available for this type'; - } else if (controller.patrolSelectionMode.value == - PatrolSelectionMode.group) { - message = 'No group patrol units available for this type'; - } else { - message = 'No patrol units available for this type'; - } +// if (filteredUnits.isEmpty) { +// // Show different messages based on the filter mode +// String message = ''; +// if (controller.patrolSelectionMode.value == +// PatrolSelectionMode.individual) { +// message = 'No individual patrol units available for this type'; +// } else if (controller.patrolSelectionMode.value == +// PatrolSelectionMode.group) { +// message = 'No group patrol units available for this type'; +// } else { +// message = 'No patrol units available for this type'; +// } - return Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(message), - const SizedBox(height: 12), - TextButton.icon( - onPressed: - () => controller.setPatrolSelectionMode( - PatrolSelectionMode.createNew, - ), - icon: Icon( - Icons.add, - color: isDark ? TColors.accent : TColors.primary, - ), - label: Text( - 'Create a new patrol unit instead?', - style: TextStyle( - fontWeight: FontWeight.bold, - color: isDark ? TColors.accent : TColors.primary, - ), - ), - ), - ], - ), - ); - } +// return Expanded( +// child: Column( +// mainAxisAlignment: MainAxisAlignment.center, +// children: [ +// Text(message), +// const SizedBox(height: 12), +// TextButton.icon( +// onPressed: +// () => controller.setPatrolSelectionMode( +// PatrolSelectionMode.createNew, +// ), +// icon: Icon( +// Icons.add, +// color: isDark ? TColors.accent : TColors.primary, +// ), +// label: Text( +// 'Create a new patrol unit instead?', +// style: TextStyle( +// fontWeight: FontWeight.bold, +// color: isDark ? TColors.accent : TColors.primary, +// ), +// ), +// ), +// ], +// ), +// ); +// } - return Expanded( - child: ListView.builder( - itemCount: filteredUnits.length, - itemBuilder: (context, index) { - final patrolUnit = filteredUnits[index]; - final isUnitSelected = - patrolUnit.id == controller.patrolUnitIdController.text; - final isCarType = patrolUnit.type.toLowerCase() == 'car'; +// return Expanded( +// child: ListView.builder( +// itemCount: filteredUnits.length, +// itemBuilder: (context, index) { +// final patrolUnit = filteredUnits[index]; +// final isUnitSelected = +// patrolUnit.id == controller.patrolUnitIdController.text; +// final isCarType = patrolUnit.type.toLowerCase() == 'car'; - return Card( - 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: - isUnitSelected - ? FontWeight.bold - : FontWeight.normal, - ), - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Members: ${patrolUnit.memberCount ?? 0}'), - if (patrolUnit.category?.isNotEmpty == true) - Text( - 'Type: ${patrolUnit.category![0].toUpperCase()}${patrolUnit.category!.substring(1)}', - style: const TextStyle(fontSize: 12), - ), - ], - ), - isThreeLine: true, - leading: Icon( - isCarType ? Icons.directions_car : Icons.motorcycle, - color: isUnitSelected ? TColors.primary : null, - ), - trailing: - isUnitSelected - ? const Icon( - Icons.check_circle, - color: TColors.primary, - ) - : null, - selected: isUnitSelected, - onTap: () => controller.joinPatrolUnit(patrolUnit), - ), - ); - }, - ), - ); - }, - ), - ], - ); - } +// return Card( +// 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: +// isUnitSelected +// ? FontWeight.bold +// : FontWeight.normal, +// ), +// ), +// subtitle: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text('Members: ${patrolUnit.memberCount ?? 0}'), +// if (patrolUnit.category?.isNotEmpty == true) +// Text( +// 'Type: ${patrolUnit.category![0].toUpperCase()}${patrolUnit.category!.substring(1)}', +// style: const TextStyle(fontSize: 12), +// ), +// ], +// ), +// isThreeLine: true, +// leading: Icon( +// isCarType ? Icons.directions_car : Icons.motorcycle, +// color: isUnitSelected ? TColors.primary : null, +// ), +// trailing: +// isUnitSelected +// ? const Icon( +// Icons.check_circle, +// color: TColors.primary, +// ) +// : null, +// selected: isUnitSelected, +// onTap: () => controller.joinPatrolUnit(patrolUnit), +// ), +// ); +// }, +// ), +// ); +// }, +// ), +// ], +// ); +// } - // Build create new patrol unit form - Widget _buildCreatePatrolUnitForm() { - final isDark = THelperFunctions.isDarkMode(Get.context!); +// // Build create new patrol unit form +// Widget _buildCreatePatrolUnitForm() { +// final isDark = THelperFunctions.isDarkMode(Get.context!); - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Create New Patrol Unit:', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), - ), - const SizedBox(height: TSizes.spaceBtwInputFields), +// return SingleChildScrollView( +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// const Text( +// 'Create New Patrol Unit:', +// style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), +// ), +// const SizedBox(height: TSizes.spaceBtwInputFields), - // Patrol Name Field - CustomTextField( - label: 'Patrol Unit Name', - controller: controller.patrolNameController, - validator: - (v) => TValidators.validateUserInput('Patrol Unit Name', v, 30), - errorText: controller.patrolNameError.value, - textInputAction: TextInputAction.next, - hintText: 'e.g., ALPHA-JBR01-C1', - prefixIcon: const Icon(Icons.label_outline), - onChanged: (value) { - controller.patrolNameController.text = value; - controller.patrolNameError.value = ''; - }, - ), +// // Patrol Name Field +// CustomTextField( +// label: 'Patrol Unit Name', +// controller: controller.patrolNameController, +// validator: +// (v) => TValidators.validateUserInput('Patrol Unit Name', v, 30), +// errorText: controller.patrolNameError.value, +// textInputAction: TextInputAction.next, +// hintText: 'e.g., ALPHA-JBR01-C1', +// prefixIcon: const Icon(Icons.label_outline), +// onChanged: (value) { +// controller.patrolNameController.text = value; +// controller.patrolNameError.value = ''; +// }, +// ), - // Error text display - Obx( - () => - controller.patrolNameError.value.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), - child: Text( - controller.patrolNameError.value, - style: TextStyle(color: Colors.red[700], fontSize: 12), - ), - ) - : const SizedBox.shrink(), - ), +// // Error text display +// Obx( +// () => +// controller.patrolNameError.value.isNotEmpty +// ? Padding( +// padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), +// child: Text( +// controller.patrolNameError.value, +// style: TextStyle(color: Colors.red[700], fontSize: 12), +// ), +// ) +// : const SizedBox.shrink(), +// ), - // Helper text for name format - Padding( - padding: const EdgeInsets.only(left: 8.0, bottom: 16.0), - child: Text( - 'Format: {CALLSIGN}-{UNIT_CODE}-{TYPE_CODE}{SEQ}', - style: TextStyle( - fontSize: 12, - color: isDark ? Colors.grey[400] : Colors.grey[600], - fontStyle: FontStyle.italic, - ), - ), - ), +// // Helper text for name format +// Padding( +// padding: const EdgeInsets.only(left: 8.0, bottom: 16.0), +// child: Text( +// 'Format: {CALLSIGN}-{UNIT_CODE}-{TYPE_CODE}{SEQ}', +// style: TextStyle( +// fontSize: 12, +// color: isDark ? Colors.grey[400] : Colors.grey[600], +// fontStyle: FontStyle.italic, +// ), +// ), +// ), - // Patrol Type Selection with expanded options - _buildPatrolTypeSelector(controller), +// // Patrol Type Selection with expanded options +// _buildPatrolTypeSelector(controller), - const SizedBox(height: TSizes.spaceBtwInputFields), +// const SizedBox(height: TSizes.spaceBtwInputFields), - // Patrol Status Selection - _buildPatrolStatusSelector(controller), +// // Patrol Status Selection +// _buildPatrolStatusSelector(controller), - const SizedBox(height: TSizes.spaceBtwInputFields), +// const SizedBox(height: TSizes.spaceBtwInputFields), - // Member Count Field with category selection - _buildMemberCountField(controller), +// // Member Count Field with category selection +// _buildMemberCountField(controller), - // Patrol Radius Field with dynamic recommendations based on type - _buildPatrolRadiusField(controller), +// // Patrol Radius Field with dynamic recommendations based on type +// _buildPatrolRadiusField(controller), - const SizedBox(height: TSizes.spaceBtwSections), +// const SizedBox(height: TSizes.spaceBtwSections), - // Show summary of the patrol unit being created - _buildPatrolUnitSummary(controller), - ], - ), - ); - } +// // Show summary of the patrol unit being created +// _buildPatrolUnitSummary(controller), +// ], +// ), +// ); +// } - // Build patrol type selector with expanded options - Widget _buildPatrolTypeSelector(OfficerInfoController controller) { - final isDark = THelperFunctions.isDarkMode(Get.context!); +// // Build patrol type selector with expanded options +// Widget _buildPatrolTypeSelector(OfficerInfoController controller) { +// final isDark = THelperFunctions.isDarkMode(Get.context!); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Patrol Type:', - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: isDark ? Colors.white : Colors.black87, - ), - ), - const SizedBox(height: 8), +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// 'Patrol Type:', +// style: TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 14, +// color: isDark ? Colors.white : Colors.black87, +// ), +// ), +// const SizedBox(height: 8), - // Wrap with GetBuilder instead of Obx for complex widget updates - GetBuilder( - id: 'patrol_type_selector', - builder: (controller) { - return Wrap( - spacing: 8, - children: - PatrolUnitTypeExtended.values.map((type) { - final isSelected = - controller.patrolTypeController.text.toLowerCase() == - type.name.toLowerCase(); +// // Wrap with GetBuilder instead of Obx for complex widget updates +// GetBuilder( +// id: 'patrol_type_selector', +// builder: (controller) { +// return Wrap( +// spacing: 8, +// children: +// PatrolUnitTypeExtended.values.map((type) { +// final isSelected = +// controller.patrolTypeController.text.toLowerCase() == +// type.name.toLowerCase(); - return ChoiceChip( - label: Text( - type.name[0].toUpperCase() + type.name.substring(1), - ), - selected: isSelected, - onSelected: (selected) { - if (selected) { - controller.patrolTypeController.text = type.name; - controller.patrolTypeError.value = ''; +// return ChoiceChip( +// label: Text( +// type.name[0].toUpperCase() + type.name.substring(1), +// ), +// selected: isSelected, +// onSelected: (selected) { +// if (selected) { +// controller.patrolTypeController.text = type.name; +// controller.patrolTypeError.value = ''; - // Update the recommended radius based on type - switch (type) { - case PatrolUnitTypeExtended.car: - controller.patrolRadiusController.text = '6500'; - break; - case PatrolUnitTypeExtended.motorcycle: - controller.patrolRadiusController.text = '4000'; - break; - case PatrolUnitTypeExtended.foot: - controller.patrolRadiusController.text = '1000'; - break; - case PatrolUnitTypeExtended.drone: - controller.patrolRadiusController.text = '3000'; - break; - case PatrolUnitTypeExtended.mixed: - controller.patrolRadiusController.text = '4000'; - break; - } - controller.update([ - 'patrol_type_selector', - 'patrol_radius_field', - 'patrol_summary', - ]); - } - }, - backgroundColor: - isDark ? Colors.grey[800] : Colors.grey[200], - selectedColor: - isDark - ? TColors.accent.withOpacity(0.3) - : TColors.primary.withOpacity(0.2), - labelStyle: TextStyle( - color: - isSelected - ? (isDark ? TColors.accent : TColors.primary) - : (isDark ? Colors.white70 : Colors.black87), - fontWeight: - isSelected ? FontWeight.bold : FontWeight.normal, - ), - shape: StadiumBorder( - side: BorderSide( - color: - isSelected - ? (isDark ? TColors.accent : TColors.primary) - : Colors.transparent, - width: 1.5, - ), - ), - avatar: - isSelected - ? Icon( - _getIconForPatrolType(type), - size: 16, - color: - isDark ? TColors.accent : TColors.primary, - ) - : null, - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - ); - }).toList(), - ); - }, - ), +// // Update the recommended radius based on type +// switch (type) { +// case PatrolUnitTypeExtended.car: +// controller.patrolRadiusController.text = '6500'; +// break; +// case PatrolUnitTypeExtended.motorcycle: +// controller.patrolRadiusController.text = '4000'; +// break; +// case PatrolUnitTypeExtended.foot: +// controller.patrolRadiusController.text = '1000'; +// break; +// case PatrolUnitTypeExtended.drone: +// controller.patrolRadiusController.text = '3000'; +// break; +// case PatrolUnitTypeExtended.mixed: +// controller.patrolRadiusController.text = '4000'; +// break; +// } +// controller.update([ +// 'patrol_type_selector', +// 'patrol_radius_field', +// 'patrol_summary', +// ]); +// } +// }, +// backgroundColor: +// isDark ? Colors.grey[800] : Colors.grey[200], +// selectedColor: +// isDark +// ? TColors.accent.withOpacity(0.3) +// : TColors.primary.withOpacity(0.2), +// labelStyle: TextStyle( +// color: +// isSelected +// ? (isDark ? TColors.accent : TColors.primary) +// : (isDark ? Colors.white70 : Colors.black87), +// fontWeight: +// isSelected ? FontWeight.bold : FontWeight.normal, +// ), +// shape: StadiumBorder( +// side: BorderSide( +// color: +// isSelected +// ? (isDark ? TColors.accent : TColors.primary) +// : Colors.transparent, +// width: 1.5, +// ), +// ), +// avatar: +// isSelected +// ? Icon( +// _getIconForPatrolType(type), +// size: 16, +// color: +// isDark ? TColors.accent : TColors.primary, +// ) +// : null, +// padding: const EdgeInsets.symmetric( +// horizontal: 12, +// vertical: 8, +// ), +// ); +// }).toList(), +// ); +// }, +// ), - // Display error text - Obx( - () => - controller.patrolTypeError.value.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), - child: Text( - controller.patrolTypeError.value, - style: TextStyle(color: Colors.red[700], fontSize: 12), - ), - ) - : const SizedBox.shrink(), - ), - ], - ); - } +// // Display error text +// Obx( +// () => +// controller.patrolTypeError.value.isNotEmpty +// ? Padding( +// padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), +// child: Text( +// controller.patrolTypeError.value, +// style: TextStyle(color: Colors.red[700], fontSize: 12), +// ), +// ) +// : const SizedBox.shrink(), +// ), +// ], +// ); +// } - // Helper to get icon for patrol type - IconData _getIconForPatrolType(PatrolUnitTypeExtended type) { - switch (type) { - case PatrolUnitTypeExtended.car: - return Icons.directions_car; - case PatrolUnitTypeExtended.motorcycle: - return Icons.motorcycle; - case PatrolUnitTypeExtended.foot: - return Icons.directions_walk; - case PatrolUnitTypeExtended.drone: - return Icons.flight; - case PatrolUnitTypeExtended.mixed: - return Icons.group; - } - } +// // Helper to get icon for patrol type +// IconData _getIconForPatrolType(PatrolUnitTypeExtended type) { +// switch (type) { +// case PatrolUnitTypeExtended.car: +// return Icons.directions_car; +// case PatrolUnitTypeExtended.motorcycle: +// return Icons.motorcycle; +// case PatrolUnitTypeExtended.foot: +// return Icons.directions_walk; +// case PatrolUnitTypeExtended.drone: +// return Icons.flight; +// case PatrolUnitTypeExtended.mixed: +// return Icons.group; +// } +// } - // Build patrol status selector - Widget _buildPatrolStatusSelector(OfficerInfoController controller) { - final isDark = THelperFunctions.isDarkMode(Get.context!); +// // Build patrol status selector +// Widget _buildPatrolStatusSelector(OfficerInfoController controller) { +// final isDark = THelperFunctions.isDarkMode(Get.context!); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Patrol Status:', - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: isDark ? Colors.white : Colors.black87, - ), - ), - const SizedBox(height: 8), +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// 'Patrol Status:', +// style: TextStyle( +// fontWeight: FontWeight.w500, +// fontSize: 14, +// color: isDark ? Colors.white : Colors.black87, +// ), +// ), +// const SizedBox(height: 8), - // Use GetBuilder instead of Obx for complex widget - GetBuilder( - id: 'patrol_status_selector', - builder: (controller) { - return Wrap( - spacing: 8, - children: - PatrolUnitStatus.values.map((status) { - // Convert enum to display text (e.g., onDuty -> On Duty) - final displayName = - status.name == 'onDuty' - ? 'On Duty' - : status.name == 'offDuty' - ? 'Off Duty' - : status.name[0].toUpperCase() + - status.name.substring(1); +// // Use GetBuilder instead of Obx for complex widget +// GetBuilder( +// id: 'patrol_status_selector', +// builder: (controller) { +// return Wrap( +// spacing: 8, +// children: +// PatrolUnitStatus.values.map((status) { +// // Convert enum to display text (e.g., onDuty -> On Duty) +// final displayName = +// status.name == 'onDuty' +// ? 'On Duty' +// : status.name == 'offDuty' +// ? 'Off Duty' +// : status.name[0].toUpperCase() + +// status.name.substring(1); - final isSelected = - controller.patrolStatusController.text.toLowerCase() == - status.name.toLowerCase(); +// final isSelected = +// controller.patrolStatusController.text.toLowerCase() == +// status.name.toLowerCase(); - return ChoiceChip( - label: Text(displayName), - selected: isSelected, - onSelected: (selected) { - if (selected) { - controller.patrolStatusController.text = status.name; - controller.patrolStatusError.value = ''; - controller.update([ - 'patrol_status_selector', - 'patrol_summary', - ]); - } - }, - backgroundColor: - isDark ? Colors.grey[800] : Colors.grey[200], - selectedColor: - isDark - ? TColors.accent.withOpacity(0.3) - : TColors.primary.withOpacity(0.2), - labelStyle: TextStyle( - color: - isSelected - ? (isDark ? TColors.accent : TColors.primary) - : (isDark ? Colors.white70 : Colors.black87), - fontWeight: - isSelected ? FontWeight.bold : FontWeight.normal, - ), - shape: StadiumBorder( - side: BorderSide( - color: - isSelected - ? (isDark ? TColors.accent : TColors.primary) - : Colors.transparent, - width: 1.5, - ), - ), - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), - ); - }).toList(), - ); - }, - ), +// return ChoiceChip( +// label: Text(displayName), +// selected: isSelected, +// onSelected: (selected) { +// if (selected) { +// controller.patrolStatusController.text = status.name; +// controller.patrolStatusError.value = ''; +// controller.update([ +// 'patrol_status_selector', +// 'patrol_summary', +// ]); +// } +// }, +// backgroundColor: +// isDark ? Colors.grey[800] : Colors.grey[200], +// selectedColor: +// isDark +// ? TColors.accent.withOpacity(0.3) +// : TColors.primary.withOpacity(0.2), +// labelStyle: TextStyle( +// color: +// isSelected +// ? (isDark ? TColors.accent : TColors.primary) +// : (isDark ? Colors.white70 : Colors.black87), +// fontWeight: +// isSelected ? FontWeight.bold : FontWeight.normal, +// ), +// shape: StadiumBorder( +// side: BorderSide( +// color: +// isSelected +// ? (isDark ? TColors.accent : TColors.primary) +// : Colors.transparent, +// width: 1.5, +// ), +// ), +// padding: const EdgeInsets.symmetric( +// horizontal: 12, +// vertical: 8, +// ), +// ); +// }).toList(), +// ); +// }, +// ), - // Display error for patrol status - Obx( - () => - controller.patrolStatusError.value.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), - child: Text( - controller.patrolStatusError.value, - style: TextStyle(color: Colors.red[700], fontSize: 12), - ), - ) - : const SizedBox.shrink(), - ), - ], - ); - } +// // Display error for patrol status +// Obx( +// () => +// controller.patrolStatusError.value.isNotEmpty +// ? Padding( +// padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), +// child: Text( +// controller.patrolStatusError.value, +// style: TextStyle(color: Colors.red[700], fontSize: 12), +// ), +// ) +// : const SizedBox.shrink(), +// ), +// ], +// ); +// } - // Build member count field with automatic category - Widget _buildMemberCountField(OfficerInfoController controller) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomTextField( - label: 'Member Count', - controller: controller.memberCountController, - keyboardType: TextInputType.number, - textInputAction: TextInputAction.next, - hintText: 'e.g., 2', - prefixIcon: const Icon(Icons.group_outlined), - errorText: controller.memberCountError.value, - onChanged: (value) { - controller.memberCountController.text = value; - controller.memberCountError.value = ''; +// // Build member count field with automatic category +// Widget _buildMemberCountField(OfficerInfoController controller) { +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// CustomTextField( +// label: 'Member Count', +// controller: controller.memberCountController, +// keyboardType: TextInputType.number, +// textInputAction: TextInputAction.next, +// hintText: 'e.g., 2', +// prefixIcon: const Icon(Icons.group_outlined), +// errorText: controller.memberCountError.value, +// onChanged: (value) { +// controller.memberCountController.text = value; +// controller.memberCountError.value = ''; - // Update category based on member count - try { - int count = int.parse(value); - controller.categoryController.text = - count > 1 ? 'group' : 'individual'; - controller.update(['member_count_category', 'patrol_summary']); - } catch (e) { - controller.categoryController.text = ''; - controller.update(['member_count_category', 'patrol_summary']); - } - }, - validator: (v) { - if (v == null || v.isEmpty) return 'Member count is required'; - try { - int count = int.parse(v); - if (count <= 0) return 'Must have at least 1 member'; - return null; - } catch (e) { - return 'Please enter a valid number'; - } - }, - ), +// // Update category based on member count +// try { +// int count = int.parse(value); +// controller.categoryController.text = +// count > 1 ? 'group' : 'individual'; +// controller.update(['member_count_category', 'patrol_summary']); +// } catch (e) { +// controller.categoryController.text = ''; +// controller.update(['member_count_category', 'patrol_summary']); +// } +// }, +// validator: (v) { +// if (v == null || v.isEmpty) return 'Member count is required'; +// try { +// int count = int.parse(v); +// if (count <= 0) return 'Must have at least 1 member'; +// return null; +// } catch (e) { +// return 'Please enter a valid number'; +// } +// }, +// ), - // Display error message - Obx( - () => - controller.memberCountError.value.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), - child: Text( - controller.memberCountError.value, - style: TextStyle(color: Colors.red[700], fontSize: 12), - ), - ) - : const SizedBox.shrink(), - ), +// // Display error message +// Obx( +// () => +// controller.memberCountError.value.isNotEmpty +// ? Padding( +// padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), +// child: Text( +// controller.memberCountError.value, +// style: TextStyle(color: Colors.red[700], fontSize: 12), +// ), +// ) +// : const SizedBox.shrink(), +// ), - // Show category based on member count - GetBuilder( - id: 'member_count_category', - builder: (controller) { - final category = controller.categoryController.text; - if (category.isEmpty) return const SizedBox.shrink(); +// // Show category based on member count +// GetBuilder( +// id: 'member_count_category', +// builder: (controller) { +// final category = controller.categoryController.text; +// if (category.isEmpty) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.only(left: 8.0, top: 4.0, bottom: 8.0), - child: Row( - children: [ - Text( - 'Category: ', - style: TextStyle(fontSize: 12, color: Colors.grey[600]), - ), - Text( - category[0].toUpperCase() + category.substring(1), - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: - category == 'group' ? TColors.primary : Colors.teal, - ), - ), - const SizedBox(width: 4), - Icon( - category == 'group' ? Icons.groups : Icons.person, - size: 14, - color: category == 'group' ? TColors.primary : Colors.teal, - ), - ], - ), - ); - }, - ), - ], - ); - } +// return Padding( +// padding: const EdgeInsets.only(left: 8.0, top: 4.0, bottom: 8.0), +// child: Row( +// children: [ +// Text( +// 'Category: ', +// style: TextStyle(fontSize: 12, color: Colors.grey[600]), +// ), +// Text( +// category[0].toUpperCase() + category.substring(1), +// style: TextStyle( +// fontWeight: FontWeight.bold, +// fontSize: 12, +// color: +// category == 'group' ? TColors.primary : Colors.teal, +// ), +// ), +// const SizedBox(width: 4), +// Icon( +// category == 'group' ? Icons.groups : Icons.person, +// size: 14, +// color: category == 'group' ? TColors.primary : Colors.teal, +// ), +// ], +// ), +// ); +// }, +// ), +// ], +// ); +// } - // Build patrol radius field with dynamic recommendations - Widget _buildPatrolRadiusField(OfficerInfoController controller) { - final isDark = THelperFunctions.isDarkMode(Get.context!); +// // Build patrol radius field with dynamic recommendations +// Widget _buildPatrolRadiusField(OfficerInfoController controller) { +// final isDark = THelperFunctions.isDarkMode(Get.context!); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomTextField( - label: 'Patrol Radius (in meters)', - controller: controller.patrolRadiusController, - validator: (v) { - if (v == null || v.isEmpty) { - return 'Radius is required'; - } - try { - final radius = double.parse(v); - if (radius <= 0) { - return 'Radius must be greater than 0'; - } +// return Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// CustomTextField( +// label: 'Patrol Radius (in meters)', +// controller: controller.patrolRadiusController, +// validator: (v) { +// if (v == null || v.isEmpty) { +// return 'Radius is required'; +// } +// try { +// final radius = double.parse(v); +// if (radius <= 0) { +// return 'Radius must be greater than 0'; +// } - // Validate against recommended ranges - final patrolType = - controller.patrolTypeController.text.toLowerCase(); - String? rangeError = _validateRadiusRange(patrolType, radius); - if (rangeError != null) return rangeError; +// // Validate against recommended ranges +// final patrolType = +// controller.patrolTypeController.text.toLowerCase(); +// String? rangeError = _validateRadiusRange(patrolType, radius); +// if (rangeError != null) return rangeError; - return null; - } catch (e) { - return 'Please enter a valid number'; - } - }, - errorText: controller.patrolRadiusError.value, - textInputAction: TextInputAction.done, - keyboardType: TextInputType.number, - hintText: '500-8000', - prefixIcon: const Icon(Icons.radio_button_checked), - onChanged: (value) { - controller.patrolRadiusController.text = value; - controller.patrolRadiusError.value = ''; - controller.update(['patrol_summary']); - }, - ), +// return null; +// } catch (e) { +// return 'Please enter a valid number'; +// } +// }, +// errorText: controller.patrolRadiusError.value, +// textInputAction: TextInputAction.done, +// keyboardType: TextInputType.number, +// hintText: '500-8000', +// prefixIcon: const Icon(Icons.radio_button_checked), +// onChanged: (value) { +// controller.patrolRadiusController.text = value; +// controller.patrolRadiusError.value = ''; +// controller.update(['patrol_summary']); +// }, +// ), - // Display error for radius - Obx( - () => - controller.patrolRadiusError.value.isNotEmpty - ? Padding( - padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), - child: Text( - controller.patrolRadiusError.value, - style: TextStyle(color: Colors.red[700], fontSize: 12), - ), - ) - : const SizedBox.shrink(), - ), +// // Display error for radius +// Obx( +// () => +// controller.patrolRadiusError.value.isNotEmpty +// ? Padding( +// padding: const EdgeInsets.only(top: 8.0, bottom: 4.0), +// child: Text( +// controller.patrolRadiusError.value, +// style: TextStyle(color: Colors.red[700], fontSize: 12), +// ), +// ) +// : const SizedBox.shrink(), +// ), - // Show recommended range based on type - GetBuilder( - id: 'patrol_radius_field', - builder: (controller) { - final patrolType = - controller.patrolTypeController.text.toLowerCase(); - if (patrolType.isEmpty) return const SizedBox.shrink(); +// // Show recommended range based on type +// GetBuilder( +// id: 'patrol_radius_field', +// builder: (controller) { +// final patrolType = +// controller.patrolTypeController.text.toLowerCase(); +// if (patrolType.isEmpty) return const SizedBox.shrink(); - final String rangeText = _getRecommendedRangeText(patrolType); +// final String rangeText = _getRecommendedRangeText(patrolType); - return Padding( - padding: const EdgeInsets.only(left: 8.0, bottom: 16.0), - child: Text( - 'Recommended range: $rangeText', - style: TextStyle( - fontSize: 12, - color: isDark ? Colors.grey[400] : Colors.grey[600], - fontStyle: FontStyle.italic, - ), - ), - ); - }, - ), - ], - ); - } +// return Padding( +// padding: const EdgeInsets.only(left: 8.0, bottom: 16.0), +// child: Text( +// 'Recommended range: $rangeText', +// style: TextStyle( +// fontSize: 12, +// color: isDark ? Colors.grey[400] : Colors.grey[600], +// fontStyle: FontStyle.italic, +// ), +// ), +// ); +// }, +// ), +// ], +// ); +// } - // Get recommended range text based on patrol type - String _getRecommendedRangeText(String patrolType) { - switch (patrolType) { - case 'car': - return '5000-8000m'; - case 'motorcycle': - return '3000-5000m'; - case 'foot': - return '500-1500m'; - case 'drone': - return '2000-4000m'; - case 'mixed': - return '2000-6000m'; - default: - return ''; - } - } +// // Get recommended range text based on patrol type +// String _getRecommendedRangeText(String patrolType) { +// switch (patrolType) { +// case 'car': +// return '5000-8000m'; +// case 'motorcycle': +// return '3000-5000m'; +// case 'foot': +// return '500-1500m'; +// case 'drone': +// return '2000-4000m'; +// case 'mixed': +// return '2000-6000m'; +// default: +// return ''; +// } +// } - // Validate radius against recommended range - String? _validateRadiusRange(String patrolType, double radius) { - switch (patrolType) { - case 'car': - if (radius < 5000) { - return 'Car patrols should have at least 5000m radius'; - } else if (radius > 8000) { - return 'Car patrol radius should not exceed 8000m'; - } - break; - case 'motorcycle': - if (radius < 3000) { - return 'Motorcycle patrols should have at least 3000m radius'; - } else if (radius > 5000) { - return 'Motorcycle patrol radius should not exceed 5000m'; - } - break; - case 'foot': - if (radius < 500) { - return 'Foot patrols should have at least 500m radius'; - } else if (radius > 1500) { - return 'Foot patrol radius should not exceed 1500m'; - } - break; - case 'drone': - if (radius < 2000) { - return 'Drone patrols should have at least 2000m radius'; - } else if (radius > 4000) { - return 'Drone patrol radius should not exceed 4000m'; - } - break; - case 'mixed': - if (radius < 2000) { - return 'Mixed patrols should have at least 2000m radius'; - } else if (radius > 6000) { - return 'Mixed patrol radius should not exceed 6000m'; - } - break; - } - return null; - } +// // Validate radius against recommended range +// String? _validateRadiusRange(String patrolType, double radius) { +// switch (patrolType) { +// case 'car': +// if (radius < 5000) { +// return 'Car patrols should have at least 5000m radius'; +// } else if (radius > 8000) { +// return 'Car patrol radius should not exceed 8000m'; +// } +// break; +// case 'motorcycle': +// if (radius < 3000) { +// return 'Motorcycle patrols should have at least 3000m radius'; +// } else if (radius > 5000) { +// return 'Motorcycle patrol radius should not exceed 5000m'; +// } +// break; +// case 'foot': +// if (radius < 500) { +// return 'Foot patrols should have at least 500m radius'; +// } else if (radius > 1500) { +// return 'Foot patrol radius should not exceed 1500m'; +// } +// break; +// case 'drone': +// if (radius < 2000) { +// return 'Drone patrols should have at least 2000m radius'; +// } else if (radius > 4000) { +// return 'Drone patrol radius should not exceed 4000m'; +// } +// break; +// case 'mixed': +// if (radius < 2000) { +// return 'Mixed patrols should have at least 2000m radius'; +// } else if (radius > 6000) { +// return 'Mixed patrol radius should not exceed 6000m'; +// } +// break; +// } +// return null; +// } - // Build summary of the patrol unit being created - Widget _buildPatrolUnitSummary(OfficerInfoController controller) { - final isDark = THelperFunctions.isDarkMode(Get.context!); - final theme = Theme.of(Get.context!); +// // Build summary of the patrol unit being created +// Widget _buildPatrolUnitSummary(OfficerInfoController controller) { +// final isDark = THelperFunctions.isDarkMode(Get.context!); +// final theme = Theme.of(Get.context!); - return GetBuilder( - id: 'patrol_summary', - builder: (controller) { - final name = controller.patrolNameController.text; - final type = controller.patrolTypeController.text; - final radius = controller.patrolRadiusController.text; - final memberCount = controller.memberCountController.text; - final category = controller.categoryController.text; - final status = controller.patrolStatusController.text; +// return GetBuilder( +// id: 'patrol_summary', +// builder: (controller) { +// final name = controller.patrolNameController.text; +// final type = controller.patrolTypeController.text; +// final radius = controller.patrolRadiusController.text; +// final memberCount = controller.memberCountController.text; +// final category = controller.categoryController.text; +// final status = controller.patrolStatusController.text; - if (name.isEmpty || - type.isEmpty || - radius.isEmpty || - memberCount.isEmpty) { - return const SizedBox.shrink(); - } +// if (name.isEmpty || +// type.isEmpty || +// radius.isEmpty || +// memberCount.isEmpty) { +// return const SizedBox.shrink(); +// } - // Generate a preview ID based on the input - final String typeCode = type.isNotEmpty ? type[0].toUpperCase() : ''; - final String unitCode = - controller.unitIdController.text.isNotEmpty - ? controller.unitIdController.text.substring( - controller.unitIdController.text.length - 2, - ) - : 'XX'; +// // Generate a preview ID based on the input +// final String typeCode = type.isNotEmpty ? type[0].toUpperCase() : ''; +// final String unitCode = +// controller.unitIdController.text.isNotEmpty +// ? controller.unitIdController.text.substring( +// controller.unitIdController.text.length - 2, +// ) +// : 'XX'; - final int rawSequence = DateTime.now().millisecondsSinceEpoch % 100000; - final String sequence = rawSequence - .toString() - .padLeft(2, '0') - .substring(0, rawSequence.toString().length.clamp(2, 5)); +// final int rawSequence = DateTime.now().millisecondsSinceEpoch % 100000; +// final String sequence = rawSequence +// .toString() +// .padLeft(2, '0') +// .substring(0, rawSequence.toString().length.clamp(2, 5)); - final previewId = 'PU-$typeCode$unitCode$sequence'; +// final previewId = 'PU-$typeCode$unitCode$sequence'; - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: - isDark - ? Colors.blueGrey[800]!.withOpacity(0.3) - : Colors.blueGrey[50]!.withOpacity(0.5), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isDark ? Colors.blueGrey[700]! : Colors.blueGrey[200]!, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Patrol Unit Preview', - style: theme.textTheme.titleMedium!.copyWith( - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 12), +// return Container( +// padding: const EdgeInsets.all(16), +// decoration: BoxDecoration( +// color: +// isDark +// ? Colors.blueGrey[800]!.withOpacity(0.3) +// : Colors.blueGrey[50]!.withOpacity(0.5), +// borderRadius: BorderRadius.circular(12), +// border: Border.all( +// color: isDark ? Colors.blueGrey[700]! : Colors.blueGrey[200]!, +// ), +// ), +// child: Column( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// Text( +// 'Patrol Unit Preview', +// style: theme.textTheme.titleMedium!.copyWith( +// fontWeight: FontWeight.bold, +// ), +// ), +// const SizedBox(height: 12), - _buildSummaryRow('ID', previewId), - _buildSummaryRow('Name', name), - _buildSummaryRow( - 'Type', - type.isNotEmpty - ? type[0].toUpperCase() + type.substring(1) - : '', - ), - _buildSummaryRow( - 'Status', - status.isNotEmpty - ? status[0].toUpperCase() + status.substring(1) - : '', - ), - _buildSummaryRow('Radius', '$radius meters'), - _buildSummaryRow('Members', memberCount), - _buildSummaryRow( - 'Category', - category.isNotEmpty - ? category[0].toUpperCase() + category.substring(1) - : '', - ), - _buildSummaryRow('Unit', controller.selectedUnitName.value), - ], - ), - ); - }, - ); - } +// _buildSummaryRow('ID', previewId), +// _buildSummaryRow('Name', name), +// _buildSummaryRow( +// 'Type', +// type.isNotEmpty +// ? type[0].toUpperCase() + type.substring(1) +// : '', +// ), +// _buildSummaryRow( +// 'Status', +// status.isNotEmpty +// ? status[0].toUpperCase() + status.substring(1) +// : '', +// ), +// _buildSummaryRow('Radius', '$radius meters'), +// _buildSummaryRow('Members', memberCount), +// _buildSummaryRow( +// 'Category', +// category.isNotEmpty +// ? category[0].toUpperCase() + category.substring(1) +// : '', +// ), +// _buildSummaryRow('Unit', controller.selectedUnitName.value), +// ], +// ), +// ); +// }, +// ); +// } - // Helper to build summary row - Widget _buildSummaryRow(String label, String value) { - if (value.isEmpty) return const SizedBox.shrink(); +// // Helper to build summary row +// Widget _buildSummaryRow(String label, String value) { +// if (value.isEmpty) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: 80, - child: Text( - '$label:', - style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14), - ), - ), - Expanded(child: Text(value, style: const TextStyle(fontSize: 14))), - ], - ), - ); - } +// return Padding( +// padding: const EdgeInsets.only(bottom: 8.0), +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.start, +// children: [ +// SizedBox( +// width: 80, +// child: Text( +// '$label:', +// style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14), +// ), +// ), +// Expanded(child: Text(value, style: const TextStyle(fontSize: 14))), +// ], +// ), +// ); +// } - // Helper method to build a selection mode tab - Widget _buildModeTab( - PatrolUnitController controller, - PatrolSelectionMode mode, - String label, - IconData icon, - ) { - final bool isSelected = controller.patrolSelectionMode.value == mode; - final isDark = THelperFunctions.isDarkMode(Get.context!); +// // Helper method to build a selection mode tab +// Widget _buildModeTab( +// PatrolUnitController controller, +// PatrolSelectionMode mode, +// String label, +// IconData icon, +// ) { +// final bool isSelected = controller.patrolSelectionMode.value == mode; +// final isDark = THelperFunctions.isDarkMode(Get.context!); - // define the color based on selection and theme - final boxDecorationColor = - isSelected - ? (isDark - ? TColors.accent.withOpacity(0.1) - : TColors.primary.withOpacity(0.1)) - : Colors.transparent; +// // define the color based on selection and theme +// final boxDecorationColor = +// isSelected +// ? (isDark +// ? TColors.accent.withOpacity(0.1) +// : TColors.primary.withOpacity(0.1)) +// : Colors.transparent; - final color = - isSelected - ? (isDark ? TColors.accent : TColors.primary) - : isDark - ? TColors.accent.withOpacity(0.3) - : TColors.primary.withOpacity(0.3); +// final color = +// isSelected +// ? (isDark ? TColors.accent : TColors.primary) +// : isDark +// ? TColors.accent.withOpacity(0.3) +// : TColors.primary.withOpacity(0.3); - return Expanded( - child: InkWell( - onTap: () => controller.setPatrolSelectionMode(mode), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: boxDecorationColor, - borderRadius: BorderRadius.circular(8), - border: Border.all(color: color), - ), - child: Column( - children: [ - Icon(icon, color: color), - const SizedBox(height: 4), - Text(label, style: TextStyle(color: color)), - ], - ), - ), - ), - ); - } -} +// return Expanded( +// child: InkWell( +// onTap: () => controller.setPatrolSelectionMode(mode), +// child: Container( +// padding: const EdgeInsets.symmetric(vertical: 12), +// decoration: BoxDecoration( +// color: boxDecorationColor, +// borderRadius: BorderRadius.circular(8), +// border: Border.all(color: color), +// ), +// child: Column( +// children: [ +// Icon(icon, color: color), +// const SizedBox(height: 4), +// Text(label, style: TextStyle(color: color)), +// ], +// ), +// ), +// ), +// ); +// } +// } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart index 027f5e0..d3974e9 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart @@ -6,6 +6,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/sel import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart'; import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart'; import 'package:sigap/src/shared/widgets/info/tips_container.dart'; +import 'package:sigap/src/shared/widgets/navigation/step_navigation_buttons.dart'; import 'package:sigap/src/shared/widgets/verification/validation_message_card.dart'; import 'package:sigap/src/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/sizes.dart'; @@ -67,11 +68,53 @@ class SelfieVerificationStep extends StatelessWidget { // Tips container _buildSelfieTips(), + + // Add space before navigation buttons + const SizedBox(height: TSizes.spaceBtwSections), + + // Navigation buttons with loading state + Obx( + () => StepNavigationButtons( + showPrevious: true, + isLastStep: false, + onPrevious: mainController.previousStep, + onNext: () => _handleNextStep(controller, mainController), + isLoading: + controller.isVerifyingFace.value || + controller.isComparingWithIDCard.value || + controller.isPerformingLivenessCheck.value, + errorMessage: controller.selfieError.value, + nextButtonText: 'Continue', + ), + ), ], ), ); } + + // Add a method to handle the next step validation + void _handleNextStep( + SelfieVerificationController controller, + FormRegistrationController mainController, + ) { + // Validate that selfie is taken and verified + if (controller.selfieImage.value == null) { + controller.selfieError.value = + 'Please complete selfie verification to continue'; + return; + } + if (!controller.isMatchWithIDCard.value && + !controller.autoVerifyForDev.value) { + controller.selfieError.value = + 'Your selfie verification must be completed successfully'; + return; + } + + // If everything is valid, go to next step + mainController.nextStep(); + } + Widget _buildDevelopmentModeIndicator(FacialVerificationService service) { if (!service.skipFaceVerification) return const SizedBox.shrink(); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart index f7a99bb..640c1db 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart @@ -26,7 +26,7 @@ class AuthButton extends StatelessWidget { onPressed: isLoading ? null : onPressed, style: ElevatedButton.styleFrom( backgroundColor: backgroundColor ?? Theme.of(context).primaryColor, - foregroundColor: textColor ?? Colors.white, + foregroundColor: textColor ?? Theme.of(context).colorScheme.onPrimary, elevation: 1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(TSizes.buttonRadius), diff --git a/sigap-mobile/lib/src/features/daily-ops/data/models/models/officers_model.dart b/sigap-mobile/lib/src/features/daily-ops/data/models/models/officers_model.dart index ebc5143..3d37c50 100644 --- a/sigap-mobile/lib/src/features/daily-ops/data/models/models/officers_model.dart +++ b/sigap-mobile/lib/src/features/daily-ops/data/models/models/officers_model.dart @@ -1,12 +1,12 @@ import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart'; class OfficerModel { - final String id; - final String unitId; - final String roleId; + final String? id; + final String? unitId; + final String? roleId; final String? patrolUnitId; - final String nrp; - final String name; + final dynamic nrp; // Changed to dynamic to handle both string and int + final String? name; final String? rank; final String? position; final String? phone; @@ -19,14 +19,20 @@ class OfficerModel { final DateTime? createdAt; final DateTime? updatedAt; final RoleModel? role; + // New fields from the JSON response + final String? bannedReason; + final DateTime? bannedUntil; + final bool isBanned; + final int panicStrike; + final int spoofingAttempts; OfficerModel({ - required this.id, - required this.unitId, - required this.roleId, + this.id, + this.unitId, + this.roleId, this.patrolUnitId, - required this.nrp, - required this.name, + this.nrp, + this.name, this.rank, this.position, this.phone, @@ -39,23 +45,28 @@ class OfficerModel { this.createdAt, this.updatedAt, this.role, + this.bannedReason, + this.bannedUntil, + this.isBanned = false, + this.panicStrike = 0, + this.spoofingAttempts = 0, }); // Create an OfficerModel instance from a JSON object factory OfficerModel.fromJson(Map json) { return OfficerModel( - id: json['id'] as String, - unitId: json['unit_id'] as String, - roleId: json['role_id'] as String, - patrolUnitId: json['patrol_unit_id'] as String?, - nrp: json['nrp'] as String, - name: json['name'] as String, - rank: json['rank'] as String?, - position: json['position'] as String?, - phone: json['phone'] as String?, - email: json['email'] as String?, - avatar: json['avatar'] as String?, - placeOfBirth: json['birth_place'] as String?, + id: json['id'], + unitId: json['unit_id'], + roleId: json['role_id'], + patrolUnitId: json['patrol_unit_id'], + nrp: json['nrp'], // Accept as dynamic + name: json['name'], + rank: json['rank'], + position: json['position'], + phone: json['phone'], + email: json['email'], + avatar: json['avatar'], + placeOfBirth: json['place_of_birth'] ?? json['birth_place'], dateOfBirth: json['date_of_birth'] != null ? DateTime.parse(json['date_of_birth'] as String) @@ -64,7 +75,7 @@ class OfficerModel { json['valid_until'] != null ? DateTime.parse(json['valid_until'] as String) : null, - qrCode: json['qr_code'] as String?, + qrCode: json['qr_code'], createdAt: json['created_at'] != null ? DateTime.parse(json['created_at'] as String) @@ -73,10 +84,15 @@ class OfficerModel { json['updated_at'] != null ? DateTime.parse(json['updated_at'] as String) : null, - role: - json['roles'] != null - ? RoleModel.fromJson(json['roles'] as Map) + // New fields + bannedReason: json['banned_reason'], + bannedUntil: + json['banned_until'] != null + ? DateTime.parse(json['banned_until'] as String) : null, + isBanned: json['is_banned'] ?? false, + panicStrike: json['panic_strike'] ?? 0, + spoofingAttempts: json['spoofing_attempts'] ?? 0, ); } @@ -94,23 +110,60 @@ class OfficerModel { 'phone': phone, 'email': email, 'avatar': avatar, - 'birth_place': placeOfBirth, + 'place_of_birth': placeOfBirth, 'date_of_birth': dateOfBirth?.toIso8601String(), 'valid_until': validUntil?.toIso8601String(), 'qr_code': qrCode, 'created_at': createdAt?.toIso8601String(), 'updated_at': updatedAt?.toIso8601String(), - if (role != null) 'roles': role!.toJson(), + 'banned_reason': bannedReason, + 'banned_until': bannedUntil?.toIso8601String(), + 'is_banned': isBanned, + 'panic_strike': panicStrike, + 'spoofing_attempts': spoofingAttempts, }; } + // Create to json non null fields + Map toJsonNonNull() { + final json = {}; + + if (id != null) json['id'] = id; + if (unitId != null) json['unit_id'] = unitId; + if (roleId != null) json['role_id'] = roleId; + if (patrolUnitId != null) json['patrol_unit_id'] = patrolUnitId; + if (nrp != null) json['nrp'] = nrp; + if (name != null) json['name'] = name; + if (rank != null) json['rank'] = rank; + if (position != null) json['position'] = position; + if (phone != null) json['phone'] = phone; + if (email != null) json['email'] = email; + if (avatar != null) json['avatar'] = avatar; + if (placeOfBirth != null) json['place_of_birth'] = placeOfBirth; + if (dateOfBirth != null) + json['date_of_birth'] = dateOfBirth!.toIso8601String(); + if (validUntil != null) json['valid_until'] = validUntil!.toIso8601String(); + if (qrCode != null) json['qr_code'] = qrCode; + if (createdAt != null) json['created_at'] = createdAt!.toIso8601String(); + if (updatedAt != null) json['updated_at'] = updatedAt!.toIso8601String(); + // New fields + if (bannedReason != null) json['banned_reason'] = bannedReason; + if (bannedUntil != null) + json['banned_until'] = bannedUntil!.toIso8601String(); + json['is_banned'] = isBanned; + json['panic_strike'] = panicStrike; + json['spoofing_attempts'] = spoofingAttempts; + + return json; + } + // Create a copy of the OfficerModel with updated fields OfficerModel copyWith({ String? id, String? unitId, String? roleId, String? patrolUnitId, - String? nrp, + dynamic nrp, // Changed to dynamic String? name, String? rank, String? position, @@ -124,6 +177,11 @@ class OfficerModel { DateTime? createdAt, DateTime? updatedAt, RoleModel? role, + String? bannedReason, + DateTime? bannedUntil, + bool? isBanned, + int? panicStrike, + int? spoofingAttempts, }) { return OfficerModel( id: id ?? this.id, @@ -144,6 +202,40 @@ class OfficerModel { createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, role: role ?? this.role, + bannedReason: bannedReason ?? this.bannedReason, + bannedUntil: bannedUntil ?? this.bannedUntil, + isBanned: isBanned ?? this.isBanned, + panicStrike: panicStrike ?? this.panicStrike, + spoofingAttempts: spoofingAttempts ?? this.spoofingAttempts, + ); + } + + // Create an empty OfficerModel + factory OfficerModel.empty() { + return OfficerModel( + id: '', + unitId: '', + roleId: '', + patrolUnitId: null, + nrp: null, + name: '', + rank: null, + position: null, + phone: null, + email: null, + avatar: null, + placeOfBirth: null, + dateOfBirth: null, + validUntil: null, + qrCode: null, + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + role: null, + bannedReason: null, + bannedUntil: null, + isBanned: false, + panicStrike: 0, + spoofingAttempts: 0, ); } @@ -178,8 +270,6 @@ class OfficerModel { ); } - - @override String toString() { return 'OfficerModel(id: $id, name: $name, nrp: $nrp)'; diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart index 06d9100..0745300 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart @@ -51,6 +51,18 @@ class RoleSelectionController extends GetxController { role.name.toLowerCase() == 'officer', ) .toList(); + + // Set default role to Viewer + if (roles.isNotEmpty) { + // Find the viewer role + final viewerRole = roles.firstWhere( + (role) => role.name.toLowerCase() == 'viewer', + orElse: () => roles[0], // Fallback to first role if viewer not found + ); + + // Select the viewer role by default + selectRole(viewerRole); + } } catch (e) { TLoaders.errorSnackBar( title: 'Error', diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart index 615f2a0..0df7a10 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart @@ -138,7 +138,7 @@ class RoleSelectionScreen extends StatelessWidget { child: ClipRRect( borderRadius: BorderRadius.circular(12), child: SvgPicture.asset( - TImages.homeOffice, + isDark ? TImages.communicationDark : TImages.communication, width: double.infinity, height: double.infinity, fit: BoxFit.cover, diff --git a/sigap-mobile/lib/src/features/personalization/data/models/models/user_metadata_model.dart b/sigap-mobile/lib/src/features/personalization/data/models/models/user_metadata_model.dart index a7d8f89..84ed7d8 100644 --- a/sigap-mobile/lib/src/features/personalization/data/models/models/user_metadata_model.dart +++ b/sigap-mobile/lib/src/features/personalization/data/models/models/user_metadata_model.dart @@ -175,7 +175,12 @@ class UserMetadataModel { ); } - /// Create copy with updated fields + /// Create a copy of this model with modified attributes + /// + /// This method allows for creating a new instance with selectively updated fields. + /// For fields not explicitly provided, the original values are retained. + /// + /// Setting 'isOfficer' will keep consistency with officer/viewer data. UserMetadataModel copyWith({ bool? isOfficer, String? userId, @@ -186,22 +191,49 @@ class UserMetadataModel { UserModel? viewerData, Map? additionalData, }) { + final newIsOfficer = isOfficer ?? this.isOfficer; + return UserMetadataModel( - isOfficer: isOfficer ?? this.isOfficer, + isOfficer: newIsOfficer, userId: userId ?? this.userId, roleId: roleId ?? this.roleId, profileStatus: profileStatus ?? this.profileStatus, email: email ?? this.email, - officerData: officerData ?? this.officerData, - viewerData: viewerData ?? this.viewerData, + // Only include officer data if the model is for an officer + officerData: newIsOfficer ? (officerData ?? this.officerData) : null, + // Only include viewer data if the model is not for an officer + viewerData: !newIsOfficer ? (viewerData ?? this.viewerData) : null, additionalData: additionalData ?? this.additionalData, ); } + /// Create a new UserMetadataModel with profile status set to 'completed' + UserMetadataModel markAsCompleted() { + return copyWith(profileStatus: 'completed'); + } + + /// Create a new UserMetadataModel with updated officer data + UserMetadataModel withUpdatedOfficerData(OfficerModel officer) { + return copyWith( + isOfficer: true, + officerData: officer, + profileStatus: 'completed', + ); + } + + /// Create a new UserMetadataModel with updated viewer data + UserMetadataModel withUpdatedViewerData(UserModel viewer) { + return copyWith( + isOfficer: false, + viewerData: viewer, + profileStatus: 'completed', + ); + } + // MARK: - Computed properties (getters) /// Primary identifier (NRP for officers, NIK for users) - String? get identifier => isOfficer ? officerData?.nrp : nik; + String? get identifier => isOfficer ? officerData?.nrp?.toString() : nik; /// User's NIK (delegated to viewerData if available) String? get nik => viewerData?.profile?.nik; @@ -211,7 +243,7 @@ class UserMetadataModel { /// User's name (delegated to appropriate model or fallback to email) String? get name { - if (isOfficer && officerData?.name.isNotEmpty == true) { + if (isOfficer && officerData?.name?.isNotEmpty == true) { return officerData!.name; } if (!isOfficer && viewerData?.profile?.fullName?.isNotEmpty == true) { @@ -238,10 +270,10 @@ class UserMetadataModel { final errors = []; if (isOfficer) { - if (officerData?.nrp.isEmpty != false) { + if (officerData?.nrp == null) { errors.add('NRP is required for officers'); } - if (officerData?.unitId.isEmpty != false) { + if (officerData?.unitId?.isEmpty != false) { errors.add('Unit ID is required for officers'); } } else { diff --git a/sigap-mobile/lib/src/features/personalization/data/repositories/officers_repository.dart b/sigap-mobile/lib/src/features/personalization/data/repositories/officers_repository.dart index 5ca95bd..378ff8c 100644 --- a/sigap-mobile/lib/src/features/personalization/data/repositories/officers_repository.dart +++ b/sigap-mobile/lib/src/features/personalization/data/repositories/officers_repository.dart @@ -1,5 +1,6 @@ import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:logger/logger.dart'; import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart'; import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart'; import 'package:sigap/src/utils/exceptions/exceptions.dart'; @@ -54,7 +55,7 @@ class OfficerRepository extends GetxController { final updatedOfficer = await _supabase - .from('officers') + .from('officers') .update(data) .eq('id', currentUserId!) .select() @@ -64,6 +65,8 @@ class OfficerRepository extends GetxController { return null; } + Logger().i('Officer updated successfully: $updatedOfficer'); + // updatedOfficer is a List, so we take the first item and convert it return OfficerModel.fromJson(updatedOfficer); } on PostgrestException catch (error) { @@ -91,6 +94,8 @@ class OfficerRepository extends GetxController { .eq('id', officerId) .single(); + Logger().i('Fetched officer data: $officerData'); + return OfficerModel.fromJson(officerData); } on PostgrestException catch (error) { throw TExceptions.fromCode(error.code!); diff --git a/sigap-mobile/lib/src/features/personalization/data/repositories/users_repository.dart b/sigap-mobile/lib/src/features/personalization/data/repositories/users_repository.dart index f073c19..b416285 100644 --- a/sigap-mobile/lib/src/features/personalization/data/repositories/users_repository.dart +++ b/sigap-mobile/lib/src/features/personalization/data/repositories/users_repository.dart @@ -116,8 +116,13 @@ class UserRepository extends GetxController { if (!isAuthenticated) { throw 'User not authenticated'; } + Logger().i('Updating user metadata: $metadata'); - await _supabase.auth.updateUser(UserAttributes(data: metadata)); + final updatedMetadata = await _supabase.auth.updateUser( + UserAttributes(data: metadata), + ); + + Logger().i('User metadata updated successfully: $updatedMetadata'); } on AuthException catch (e) { _logger.e('AuthException in updateUserMetadata: ${e.message}'); throw TExceptions(e.message); @@ -127,6 +132,27 @@ class UserRepository extends GetxController { } } + // update profile status in user metadata + Future updateProfileStatus(String status) async { + try { + if (!isAuthenticated) { + throw 'User not authenticated'; + } + + final metadata = { + 'profile_status': status, + }; + + await updateUserMetadata(metadata); + } on AuthException catch (e) { + _logger.e('AuthException in updateProfileStatus: ${e.message}'); + throw TExceptions(e.message); + } catch (e) { + _logger.e('Exception in updateProfileStatus: $e'); + throw 'Failed to update profile status: ${e.toString()}'; + } + } + // Update user email Future updateUserEmail(String newEmail) async { try { 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 60a255e..a9dd238 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 @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:ui'; // Add this import for ImageFilter import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; @@ -379,43 +380,69 @@ class ImageUploader extends StatelessWidget { } Widget _defaultErrorOverlay() { - return Container( - height: 200, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2), - color: TColors.error.withOpacity(0.2), - ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.error_outline, - color: TColors.error, - size: TSizes.iconLg, - ), - const SizedBox(height: TSizes.sm), - Text( - 'Invalid Image', - style: TextStyle( - color: TColors.error, - fontWeight: FontWeight.bold, + return ClipRRect( + borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), + child: Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2), + color: Colors.black.withOpacity(0.5), + ), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + TColors.error.withOpacity(0.4), + Colors.black.withOpacity(0.7), + ], ), ), - const SizedBox(height: TSizes.xs), - Padding( - padding: const EdgeInsets.symmetric(horizontal: TSizes.md), - child: Text( - errorMessage ?? 'Please try another image', - textAlign: TextAlign.center, - style: TextStyle( - color: TColors.error, - fontSize: TSizes.fontSizeSm, - ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: TColors.error.withOpacity(0.2), + shape: BoxShape.circle, + ), + child: Icon( + Icons.error_outline, + color: Colors.white, + size: TSizes.iconLg, + ), + ), + const SizedBox(height: TSizes.sm), + Text( + 'Invalid Image', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + const SizedBox(height: TSizes.xs), + Padding( + padding: const EdgeInsets.symmetric(horizontal: TSizes.md), + child: Text( + errorMessage ?? 'Please try another image', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white.withOpacity(0.9), + fontSize: TSizes.fontSizeSm, + ), + ), + ), + ], ), ), - ], + ), ), ), ); @@ -447,4 +474,5 @@ class ImageUploader extends StatelessWidget { ), ); } + } diff --git a/sigap-mobile/lib/src/shared/widgets/navigation/step_navigation_buttons.dart b/sigap-mobile/lib/src/shared/widgets/navigation/step_navigation_buttons.dart new file mode 100644 index 0000000..cfbd32d --- /dev/null +++ b/sigap-mobile/lib/src/shared/widgets/navigation/step_navigation_buttons.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; +import 'package:sigap/src/utils/constants/colors.dart'; +import 'package:sigap/src/utils/constants/sizes.dart'; + +class StepNavigationButtons extends StatelessWidget { + final bool showPrevious; + final bool isLastStep; + final VoidCallback onPrevious; + final VoidCallback onNext; + final bool isLoading; + final String? errorMessage; + final String nextButtonText; + final String previousButtonText; + final bool + useDefaultNavigation; // New parameter for custom navigation control + + const StepNavigationButtons({ + super.key, + this.showPrevious = true, + this.isLastStep = false, + required this.onPrevious, + required this.onNext, + this.isLoading = false, + this.errorMessage, + this.nextButtonText = 'Next', + this.previousButtonText = 'Previous', + this.useDefaultNavigation = + true, // Default to true for backward compatibility + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // Error message if any + if (errorMessage != null && errorMessage!.isNotEmpty) + Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: TSizes.sm), + padding: const EdgeInsets.all(TSizes.sm), + decoration: BoxDecoration( + color: TColors.error.withOpacity(0.1), + borderRadius: BorderRadius.circular(TSizes.borderRadiusSm), + border: Border.all(color: TColors.error), + ), + child: Row( + children: [ + Icon(Icons.error_outline, color: TColors.error, size: 20), + const SizedBox(width: TSizes.sm), + Expanded( + child: Text( + errorMessage!, + style: TextStyle(color: TColors.error, fontSize: 13), + ), + ), + ], + ), + ), + + // Navigation buttons + Row( + children: [ + // Back button + if (showPrevious) + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: TSizes.sm), + child: ElevatedButton( + // Use onPrevious only if not loading and either useDefaultNavigation is true or we're handling custom navigation + onPressed: isLoading ? null : onPrevious, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.grey[300], + foregroundColor: Colors.black87, + padding: const EdgeInsets.symmetric(vertical: 16), + disabledBackgroundColor: Colors.grey[200], + disabledForegroundColor: Colors.grey, + ), + child: Text(previousButtonText), + ), + ), + ), + + // Next/Submit button + Expanded( + child: ElevatedButton( + // For next button, respect the useDefaultNavigation flag + onPressed: + isLoading + ? null + : () { + // If using default navigation or validation logic, just call the provided callback + if (useDefaultNavigation) { + onNext(); + } else { + // For custom navigation, the caller is responsible for form validation + // and navigation logic in the onNext callback + onNext(); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: TColors.primary, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + disabledBackgroundColor: TColors.primary.withOpacity(0.3), + ), + child: + isLoading + ? const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ), + SizedBox(width: TSizes.xs), + ], + ) + : Text(isLastStep ? 'Submit' : nextButtonText), + ), + ), + ], + ), + ], + ); + } +} diff --git a/sigap-mobile/lib/src/shared/widgets/state_screeen/state_screen.dart b/sigap-mobile/lib/src/shared/widgets/state_screeen/state_screen.dart index 30eb0cf..a795590 100644 --- a/sigap-mobile/lib/src/shared/widgets/state_screeen/state_screen.dart +++ b/sigap-mobile/lib/src/shared/widgets/state_screeen/state_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:lottie/lottie.dart'; import 'package:sigap/src/shared/styles/spacing_styles.dart'; import 'package:sigap/src/utils/constants/colors.dart'; @@ -20,6 +21,7 @@ class StateScreen extends StatelessWidget { this.primaryButtonTitle = 'Continue', this.onSecondaryPressed, this.isLottie = false, + this.isSvg = false, }); final String? image; @@ -28,6 +30,7 @@ class StateScreen extends StatelessWidget { final String primaryButtonTitle; final String secondaryTitle; final bool? isLottie; + final bool isSvg; final VoidCallback? onPressed; final VoidCallback? onSecondaryPressed; final bool showButton; @@ -50,7 +53,7 @@ class StateScreen extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Image, Icon, or Lottie + // Image, Icon, Lottie, or SVG if (icon != null) Icon( icon, @@ -62,6 +65,15 @@ class StateScreen extends StatelessWidget { image!, width: THelperFunctions.screenWidth() * 0.8, ) + else if (isSvg && image != null) + SvgPicture.asset( + image!, + width: THelperFunctions.screenWidth() * 0.8, + colorFilter: const ColorFilter.mode( + TColors.primary, + BlendMode.srcIn, + ), + ) else if (image != null) Image( image: AssetImage(image!), diff --git a/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart b/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart index 886c136..91b0528 100644 --- a/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart +++ b/sigap-mobile/lib/src/shared/widgets/text/custom_text_field.dart @@ -20,6 +20,7 @@ class CustomTextField extends StatelessWidget { final void Function(String)? onChanged; final Color? accentColor; final Color? fillColor; + final InputDecoration? decoration; // New parameter for custom decoration const CustomTextField({ super.key, @@ -40,8 +41,8 @@ class CustomTextField extends StatelessWidget { this.onChanged, this.accentColor, this.fillColor, + this.decoration, // Add to constructor }) : assert( - // Fix the assertion to avoid duplicate conditions controller == null || initialValue == null, 'Either provide a controller or an initialValue, not both', ); @@ -59,8 +60,10 @@ class CustomTextField extends StatelessWidget { ? (isDark ? Colors.grey[800]! : Colors.grey[200]!) : fillColor ?? (isDark ? TColors.dark : TColors.lightContainer); - // Get the common input decoration for both cases - final inputDecoration = _getInputDecoration( + // Get the input decoration - either custom or default + final inputDecoration = + decoration ?? + _getInputDecoration( context, effectiveAccentColor, isDark, diff --git a/sigap-mobile/lib/src/utils/constants/num_int.dart b/sigap-mobile/lib/src/utils/constants/num_int.dart index 4bf037d..49aced1 100644 --- a/sigap-mobile/lib/src/utils/constants/num_int.dart +++ b/sigap-mobile/lib/src/utils/constants/num_int.dart @@ -2,5 +2,6 @@ class TNum { // Auth Number static const int oneTimePassword = 6; static const int totalStepViewer = 4; - static const int totalStepOfficer = 4; + static const int totalStepOfficer = + 3; // Reduced from 4 to 3 steps for officers } diff --git a/sigap-website/prisma/schema.prisma b/sigap-website/prisma/schema.prisma index 3af0dc8..18720e6 100644 --- a/sigap-website/prisma/schema.prisma +++ b/sigap-website/prisma/schema.prisma @@ -326,12 +326,12 @@ model patrol_units { 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) + category patrol_unit_category? @default(group) + member_count Int? @default(0) 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)