feat: Enhance registration flow with navigation buttons and error handling

This commit is contained in:
vergiLgood1 2025-05-27 01:53:29 +07:00
parent 569e1d6049
commit b6a82438dd
10 changed files with 106 additions and 44 deletions

View File

@ -455,18 +455,19 @@ class AzureOCRService {
if (gender.contains('laki') || if (gender.contains('laki') ||
gender.contains('pria') || gender.contains('pria') ||
gender == 'l' || gender == 'l' ||
gender == 'male') { gender == 'male' ||
extractedInfo['gender'] = 'Male'; gender == 'm') {
extractedInfo['jenis_kelamin'] = 'LAKI-LAKI'; extractedInfo['jenis_kelamin'] = 'LAKI-LAKI';
} else if (gender.contains('perempuan') || } else if (gender.contains('perempuan') ||
gender.contains('wanita') || gender.contains('wanita') ||
gender == 'p' || gender == 'p' ||
gender == 'female') { gender == 'female' ||
extractedInfo['gender'] = 'Female'; gender == 'f' ||
gender == 'w') {
extractedInfo['jenis_kelamin'] = 'PEREMPUAN'; extractedInfo['jenis_kelamin'] = 'PEREMPUAN';
} else { } else {
extractedInfo['gender'] = _normalizeCase(genderText); // Default to LAKI-LAKI if gender can't be determined
extractedInfo['jenis_kelamin'] = _normalizeCase(genderText); extractedInfo['jenis_kelamin'] = 'LAKI-LAKI';
} }
} }
@ -1411,7 +1412,7 @@ class AzureOCRService {
// Check if KTP has all required fields // Check if KTP has all required fields
bool isKtpValid(Map<String, String> extractedInfo) { bool isKtpValid(Map<String, String> extractedInfo) {
// Required fields for KTP validation // Required fields for KTP validation
final requiredFields = ['nik', 'nama', 'jenis_kelamin', 'kewarganegaraan']; final requiredFields = ['nik', 'nama', 'kewarganegaraan'];
// Check that all required fields are present and not empty // Check that all required fields are present and not empty
for (var field in requiredFields) { for (var field in requiredFields) {
@ -1452,17 +1453,20 @@ class AzureOCRService {
// } // }
// // Gender should be either "L" or "P" // // Gender should be either "L" or "P"
final gender = extractedInfo['jenis_kelamin'] ?? ''; // final gender = extractedInfo['jenis_kelamin'] ?? '';
if (gender.isEmpty || gender != 'LAKI-LAKI' || gender != 'PEREMPUAN') {
print('KTP validation failed: Missing or invalid gender'); // if (gender.isEmpty || (gender != 'LAKI-LAKI' && gender != 'PEREMPUAN')) {
return false; // print('KTP validation failed: Missing or invalid gender');
} // return false;
// }
// // Nationality should be "WNI" // // Nationality should be "WNI"
final nationality = extractedInfo['kewarganegaraan'] ?? 'WNI'; final nationality = extractedInfo['kewarganegaraan'] ?? 'Wbi';
if (nationality.isEmpty || nationality != 'WNI') { print('Nationality: $nationality');
print('KTP validation failed: Missing or invalid');
if (nationality.isEmpty || nationality != 'Wni') {
print('KTP validation failed: Missing or invalid nationality');
return false; return false;
} }

View File

@ -19,6 +19,7 @@ import 'package:sigap/src/features/personalization/data/repositories/profile_rep
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart'; import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart'; import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart';
import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart'; import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart';
import 'package:sigap/src/utils/constants/image_strings.dart';
import 'package:sigap/src/utils/constants/num_int.dart'; import 'package:sigap/src/utils/constants/num_int.dart';
import 'package:sigap/src/utils/helpers/network_manager.dart'; import 'package:sigap/src/utils/helpers/network_manager.dart';
import 'package:sigap/src/utils/popups/loaders.dart'; import 'package:sigap/src/utils/popups/loaders.dart';
@ -74,6 +75,8 @@ class FormRegistrationController extends GetxController {
final RxString submitMessage = RxString(''); final RxString submitMessage = RxString('');
final RxBool isSubmitSuccess = RxBool(false); final RxBool isSubmitSuccess = RxBool(false);
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
@ -714,6 +717,17 @@ class FormRegistrationController extends GetxController {
try { try {
isSubmitting.value = true; isSubmitting.value = true;
// validate all controllers before proceeding
// if (!formKey!.currentState!.validate()) {
// TLoaders.errorSnackBar(
// title: 'Validation Error',
// message: 'Please fill in all required fields correctly.',
// );
// isSubmitting.value = false;
// submitMessage.value = 'Please fill in all required fields correctly.';
// return;
// }
final isConnected = await NetworkManager.instance.isConnected(); final isConnected = await NetworkManager.instance.isConnected();
if (!isConnected) { if (!isConnected) {
TLoaders.errorSnackBar( TLoaders.errorSnackBar(
@ -721,6 +735,8 @@ class FormRegistrationController extends GetxController {
message: 'Please check your internet connection and try again.', message: 'Please check your internet connection and try again.',
); );
isSubmitting.value = false; isSubmitting.value = false;
submitMessage.value =
'Failed to complete registration: No internet connection';
return; return;
} }
@ -744,8 +760,6 @@ class FormRegistrationController extends GetxController {
birthDate: _parseBirthDate(identityController.birthDateController.text), birthDate: _parseBirthDate(identityController.birthDateController.text),
); );
Logger().d('Created user model for update: ${updateUser.toJson()}');
// Check if NIK is already registered (but ignore if it's the current user's NIK) // Check if NIK is already registered (but ignore if it's the current user's NIK)
final isNikTaken = await UserRepository.instance.isNikExists( final isNikTaken = await UserRepository.instance.isNikExists(
updateProfile!.nik!, updateProfile!.nik!,
@ -757,6 +771,7 @@ class FormRegistrationController extends GetxController {
message: 'NIK already registered to another user!', message: 'NIK already registered to another user!',
); );
isSubmitting.value = false; isSubmitting.value = false;
submitMessage.value = 'NIK already registered to another user!';
return; return;
} }
@ -773,28 +788,27 @@ class FormRegistrationController extends GetxController {
message: 'Failed to update user profile. Please try again.', message: 'Failed to update user profile. Please try again.',
); );
isSubmitting.value = false; isSubmitting.value = false;
submitMessage.value =
'Failed to update user profile. Please try again.';
return; return;
} }
// Update user metadata with common profile info
final metadata =
UserMetadataModel(profileStatus: 'completed').toAuthMetadataJson();
// Update metadata first // Update metadata first
await userRepository.updateUserMetadata(metadata); await userRepository.updateProfileStatus('completed');
Logger().d('User metadata updated successfully'); Logger().d('User metadata updated successfully');
isSubmitSuccess.value = true; isSubmitSuccess.value = true;
submitMessage.value = 'Registration completed successfully'; // submitMessage.value = 'Registration completed successfully';
Get.off( Get.off(
() => StateScreen( () => StateScreen(
title: 'Registration Completed', title: 'Registration Completed',
subtitle: 'Your registration has been successfully completed.', subtitle: 'Your registration has been successfully completed.',
icon: Icons.check_circle, image: TImages.womanHuggingEarth,
isSvg: true,
showButton: true, showButton: true,
primaryButtonTitle: 'Go to Home', primaryButtonTitle: 'Back to sign in',
onPressed: () => AuthenticationRepository.instance.screenRedirect(), onPressed: () => AuthenticationRepository.instance.screenRedirect(),
), ),
); );

View File

@ -546,15 +546,8 @@ class IdentityVerificationController extends GetxController {
// Save registration data using the centralized model // Save registration data using the centralized model
void saveRegistrationData() async { void saveRegistrationData() async {
try { try {
// Call the appropriate registration update function based on user type
if (isOfficer) {
// For officers, use the officer registration update function
// mainController.updateOfficerRegistrationData();
}
// For regular users, call the three required updates // For regular users, call the three required updates
mainController.updateUserRegistrationData(); mainController.updateUserRegistrationData();
} catch (e) { } catch (e) {
isDataSaved.value = false; isDataSaved.value = false;
dataSaveMessage.value = 'Error saving registration data: $e'; dataSaveMessage.value = 'Error saving registration data: $e';
@ -564,8 +557,6 @@ class IdentityVerificationController extends GetxController {
} }
} }
@override @override
void onClose() { void onClose() {
// Dispose form controllers // Dispose form controllers

View File

@ -48,7 +48,7 @@ class FacialVerificationService {
final detectedFaces = await _edgeFunctionService.detectFaces(image); final detectedFaces = await _edgeFunctionService.detectFaces(image);
return detectedFaces.isNotEmpty; return detectedFaces.isNotEmpty;
} catch (e) { } catch (e) {
print('Error detecting face: $e'); print('Error detecting face. Please try again later.');
return false; return false;
} }
} }

View File

@ -280,7 +280,7 @@ class SelfieVerificationController extends GetxController {
await _compareWithIdCard(); await _compareWithIdCard();
} catch (e) { } catch (e) {
dev.log('Face detection API error: $e', name: 'SELFIE_VERIFICATION'); dev.log('Face detection API error: $e', name: 'SELFIE_VERIFICATION');
selfieError.value = 'Error detecting face: $e'; selfieError.value = 'Error detecting face: Please try again.';
isVerifyingFace.value = false; isVerifyingFace.value = false;
isSelfieValid.value = false; isSelfieValid.value = false;
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
import 'package:sigap/src/utils/validators/validation.dart'; import 'package:sigap/src/utils/validators/validation.dart';
class PersonalInfoController extends GetxController { class PersonalInfoController extends GetxController {
@ -23,6 +24,8 @@ class PersonalInfoController extends GetxController {
final RxString bioError = ''.obs; final RxString bioError = ''.obs;
final RxString addressError = ''.obs; final RxString addressError = ''.obs;
final RxString personalInfoError = ''.obs; // General error for personal info
// Manual validation as fallback // Manual validation as fallback
final RxBool isFormValid = RxBool(true); final RxBool isFormValid = RxBool(true);
@ -113,6 +116,18 @@ class PersonalInfoController extends GetxController {
return isFormValid.value; return isFormValid.value;
} }
void handleNextStep(
GlobalKey<FormState> formKey,
FormRegistrationController mainController,
) {
if (!validate(formKey)) {
personalInfoError.value = 'Please fill the required information.';
return;
}
mainController.nextStep();
}
void clearErrors() { void clearErrors() {
firstNameError.value = ''; firstNameError.value = '';
lastNameError.value = ''; lastNameError.value = '';

View File

@ -97,13 +97,23 @@ class FormRegistrationScreen extends StatelessWidget {
currentStep: controller.currentStep.value, currentStep: controller.currentStep.value,
totalSteps: controller.totalSteps, totalSteps: controller.totalSteps,
stepTitles: controller.getStepTitles(), stepTitles: controller.getStepTitles(),
onStepTapped: controller.goToStep, onStepTapped: (index) {
// If stepping forward, use nextStep() to validate
if (index > controller.currentStep.value) {
controller.nextStep();
} else {
// If stepping backward, just go to that step without validation
controller.goToStep(index);
}
},
style: StepIndicatorStyle.standard, style: StepIndicatorStyle.standard,
), ),
), ),
); );
} }
// Add navigation buttons
Widget _buildStepContent(FormRegistrationController controller) { Widget _buildStepContent(FormRegistrationController controller) {
final isOfficer = controller.selectedRole.value?.isOfficer ?? false; final isOfficer = controller.selectedRole.value?.isOfficer ?? false;

View File

@ -8,6 +8,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/off
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/viewer-information/personal_info_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/viewer-information/personal_info_controller.dart';
import 'package:sigap/src/shared/widgets/form/form_section_header.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/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart'; import 'package:sigap/src/utils/constants/sizes.dart';
@ -58,7 +59,7 @@ class IdentityVerificationStep extends StatelessWidget {
onPressed: onPressed:
ctrl.isSavingData.value ctrl.isSavingData.value
? null ? null
: () => _submitRegistrationData(controller, context), : () => _submitRegistrationData(controller),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: TColors.primary, backgroundColor: TColors.primary,
foregroundColor: Colors.white, foregroundColor: Colors.white,
@ -89,6 +90,18 @@ class IdentityVerificationStep extends StatelessWidget {
), ),
), ),
Obx(
() => StepNavigationButtons(
showPrevious: mainController.currentStep.value > 0,
isLastStep: false,
onPrevious: mainController.previousStep,
onNext: () => _submitRegistrationData(controller),
isLoading: mainController.isSubmitting.value,
errorMessage: mainController.submitMessage.value,
nextButtonText: 'Continue',
),
),
// Save Result Message // Save Result Message
GetBuilder<IdentityVerificationController>( GetBuilder<IdentityVerificationController>(
id: 'saveMessage', id: 'saveMessage',
@ -474,10 +487,8 @@ class IdentityVerificationStep extends StatelessWidget {
// Submit registration data // Submit registration data
void _submitRegistrationData( void _submitRegistrationData(
IdentityVerificationController controller, IdentityVerificationController controller,
BuildContext context,
) async { ) async {
// Call saveRegistrati`onData with all the collected data
// Call saveRegistrationData with all the collected data
controller.saveRegistrationData(); controller.saveRegistrationData();
// The rest of submission logic would be handled in the controller // The rest of submission logic would be handled in the controller

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/viewer-information/personal_info_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/viewer-information/personal_info_controller.dart';
import 'package:sigap/src/shared/widgets/form/form_section_header.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/shared/widgets/text/custom_text_field.dart';
import 'package:sigap/src/utils/constants/sizes.dart'; import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/validators/validation.dart'; import 'package:sigap/src/utils/validators/validation.dart';
@ -127,6 +128,20 @@ class PersonalInfoStep extends StatelessWidget {
}, },
), ),
), ),
const SizedBox(height: TSizes.spaceBtwSections),
// Navigation buttons
Obx(
() => StepNavigationButtons(
showPrevious: mainController.currentStep.value > 0,
isLastStep: false,
onPrevious: mainController.previousStep,
onNext: () => controller.handleNextStep(formKey, mainController),
errorMessage: controller.personalInfoError.value,
nextButtonText: 'Continue',
)
),
], ],
), ),
); );

View File

@ -805,6 +805,8 @@ class SelfieVerificationStep extends StatelessWidget {
SelfieVerificationController controller, SelfieVerificationController controller,
BuildContext context, BuildContext context,
) { ) {
final isDark = THelperFunctions.isDarkMode(context);
switch (status) { switch (status) {
case VerificationStatus.initial: case VerificationStatus.initial:
case VerificationStatus.performingLiveness: case VerificationStatus.performingLiveness:
@ -817,7 +819,7 @@ class SelfieVerificationStep extends StatelessWidget {
isLoading: true, isLoading: true,
title: 'Detecting Face', title: 'Detecting Face',
icon: Icons.face_retouching_natural, icon: Icons.face_retouching_natural,
customColor: TColors.primary, customColor: isDark ? TColors.accent : TColors.primary,
); );
case VerificationStatus.comparingWithID: case VerificationStatus.comparingWithID:
@ -827,7 +829,7 @@ class SelfieVerificationStep extends StatelessWidget {
isLoading: true, isLoading: true,
title: 'Face Matching', title: 'Face Matching',
icon: Icons.compare, icon: Icons.compare,
customColor: TColors.primary, customColor: isDark ? TColors.accent : TColors.primary,
); );
case VerificationStatus.livenessCompleted: case VerificationStatus.livenessCompleted: