Refactor identity verification process and enhance registration summary
- Updated IdentityVerificationController to streamline data loading and validation. - Introduced a new VerificationSummary widget for displaying registration details. - Improved form validation logic to ensure all required fields are checked before submission. - Enhanced user profile update functionality in UserRepository to include additional registration data. - Added verification progress card to provide visual feedback on the status of verification steps. - Cleaned up code for better readability and maintainability.
This commit is contained in:
parent
5c3faac8c3
commit
7ca33cdaa3
|
@ -14,6 +14,7 @@ import 'package:sigap/src/features/daily-ops/data/models/index.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
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/utils/constants/num_int.dart';
|
import 'package:sigap/src/utils/constants/num_int.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
||||||
|
@ -83,7 +84,7 @@ class FormRegistrationController extends GetxController {
|
||||||
try {
|
try {
|
||||||
Logger().d('Fetching user data safely without redirects');
|
Logger().d('Fetching user data safely without redirects');
|
||||||
|
|
||||||
// Get user session directly without going through AuthRepository methods that might trigger redirects
|
// Get user session directly without going through UserRepositorysitory methods that might trigger redirects
|
||||||
final session = SupabaseService.instance.client.auth.currentSession;
|
final session = SupabaseService.instance.client.auth.currentSession;
|
||||||
|
|
||||||
if (session?.user != null) {
|
if (session?.user != null) {
|
||||||
|
@ -691,8 +692,7 @@ class FormRegistrationController extends GetxController {
|
||||||
submitMessage.value = 'Submitting your registration...';
|
submitMessage.value = 'Submitting your registration...';
|
||||||
|
|
||||||
// Save all registration data using the identity verification controller
|
// Save all registration data using the identity verification controller
|
||||||
final identityController = Get.find<IdentityVerificationController>();
|
final result = await saveRegistrationData();
|
||||||
final result = await identityController.saveRegistrationData();
|
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
isSubmitSuccess.value = true;
|
isSubmitSuccess.value = true;
|
||||||
|
@ -712,4 +712,155 @@ class FormRegistrationController extends GetxController {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Save registration data by collecting information from all steps
|
||||||
|
Future<bool> saveRegistrationData({Map<String, dynamic>? summaryData}) async {
|
||||||
|
try {
|
||||||
|
Logger().d('Starting registration data save process');
|
||||||
|
|
||||||
|
// Collect all data from forms if not provided in summaryData
|
||||||
|
if (summaryData == null) {
|
||||||
|
collectAllFormData();
|
||||||
|
Logger().d('Collected data from all form controllers');
|
||||||
|
} else {
|
||||||
|
Logger().d('Using provided summary data for registration');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the final data object for submission based on user models
|
||||||
|
Map<String, dynamic> finalData = {};
|
||||||
|
|
||||||
|
// Basic user information (matching UserModel structure)
|
||||||
|
finalData['user_id'] = userMetadata.value.userId;
|
||||||
|
finalData['email'] = userMetadata.value.email;
|
||||||
|
finalData['role_id'] =
|
||||||
|
userMetadata.value.roleId ?? selectedRole.value?.id;
|
||||||
|
finalData['is_officer'] = userMetadata.value.isOfficer;
|
||||||
|
finalData['phone'] = personalInfoController.phoneController.text;
|
||||||
|
finalData['profile_status'] = 'pending_approval';
|
||||||
|
|
||||||
|
// Structure user profile data according to ProfileModel
|
||||||
|
Map<String, dynamic> profileData = {};
|
||||||
|
if (!userMetadata.value.isOfficer) {
|
||||||
|
// Regular user profile data (matching ProfileModel structure)
|
||||||
|
profileData = {
|
||||||
|
'user_id': userMetadata.value.userId,
|
||||||
|
'nik': identityController.nikController.text,
|
||||||
|
'first_name': personalInfoController.firstNameController.text,
|
||||||
|
'last_name': personalInfoController.lastNameController.text,
|
||||||
|
'place_of_birth': identityController.placeOfBirthController.text,
|
||||||
|
'birth_date': identityController.birthDateController.text,
|
||||||
|
'address': {'address': personalInfoController.addressController.text},
|
||||||
|
};
|
||||||
|
finalData['profile'] = profileData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For officers, structure data according to OfficerModel
|
||||||
|
if (userMetadata.value.isOfficer) {
|
||||||
|
// Officer data (matching OfficerModel structure)
|
||||||
|
Map<String, dynamic> officerData = {
|
||||||
|
'id': userMetadata.value.userId,
|
||||||
|
'unit_id': unitInfoController?.unitIdController.text ?? '',
|
||||||
|
'role_id': finalData['role_id'],
|
||||||
|
'nrp':
|
||||||
|
identityController
|
||||||
|
.nikController
|
||||||
|
.text, // NRP is stored in NIK field for officers
|
||||||
|
'name': personalInfoController.nameController.text,
|
||||||
|
'rank': officerInfoController?.rankController.text,
|
||||||
|
'position': unitInfoController?.positionController.text,
|
||||||
|
'phone': personalInfoController.phoneController.text,
|
||||||
|
'email': finalData['email'],
|
||||||
|
'place_of_birth': identityController.placeOfBirthController.text,
|
||||||
|
'date_of_birth': identityController.birthDateController.text,
|
||||||
|
};
|
||||||
|
finalData['officer'] = officerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store ID card verification data (KTP or KTA)
|
||||||
|
if (userMetadata.value.isOfficer) {
|
||||||
|
// KTA data
|
||||||
|
finalData['id_card'] = {
|
||||||
|
'type': 'KTA',
|
||||||
|
'nrp': identityController.nikController.text,
|
||||||
|
'name': identityController.fullNameController.text,
|
||||||
|
'birth_date': identityController.birthDateController.text,
|
||||||
|
'gender': identityController.selectedGender.value,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// KTP data
|
||||||
|
finalData['id_card'] = {
|
||||||
|
'type': 'KTP',
|
||||||
|
'nik': identityController.nikController.text,
|
||||||
|
'name': identityController.fullNameController.text,
|
||||||
|
'place_of_birth': identityController.placeOfBirthController.text,
|
||||||
|
'birth_date': identityController.birthDateController.text,
|
||||||
|
'gender': identityController.selectedGender.value,
|
||||||
|
'address': identityController.addressController.text,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Face verification data
|
||||||
|
finalData['face_verification'] = {
|
||||||
|
'selfie_valid': selfieVerificationController.isSelfieValid.value,
|
||||||
|
'liveness_check_passed':
|
||||||
|
selfieVerificationController.isLivenessCheckPassed.value,
|
||||||
|
'face_match_result':
|
||||||
|
selfieVerificationController.isMatchWithIDCard.value,
|
||||||
|
'match_confidence': selfieVerificationController.matchConfidence.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Image paths
|
||||||
|
finalData['images'] = {
|
||||||
|
'id_card': idCardVerificationController.idCardImage.value?.path,
|
||||||
|
'selfie': selfieVerificationController.selfieImage.value?.path,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge with provided summary data if available
|
||||||
|
if (summaryData != null && summaryData.isNotEmpty) {
|
||||||
|
// Merge summaryData into profile or officer data based on user type
|
||||||
|
if (userMetadata.value.isOfficer) {
|
||||||
|
if (summaryData['fullName'] != null) {
|
||||||
|
finalData['officer']['name'] = summaryData['fullName'];
|
||||||
|
}
|
||||||
|
if (summaryData['birthDate'] != null) {
|
||||||
|
finalData['officer']['date_of_birth'] = summaryData['birthDate'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (summaryData['fullName'] != null) {
|
||||||
|
final names = summaryData['fullName'].toString().split(' ');
|
||||||
|
if (names.isNotEmpty) {
|
||||||
|
finalData['profile']['first_name'] = names.first;
|
||||||
|
if (names.length > 1) {
|
||||||
|
finalData['profile']['last_name'] = names.sublist(1).join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (summaryData['placeOfBirth'] != null) {
|
||||||
|
finalData['profile']['place_of_birth'] =
|
||||||
|
summaryData['placeOfBirth'];
|
||||||
|
}
|
||||||
|
if (summaryData['birthDate'] != null) {
|
||||||
|
finalData['profile']['birth_date'] = summaryData['birthDate'];
|
||||||
|
}
|
||||||
|
if (summaryData['address'] != null) {
|
||||||
|
finalData['profile']['address'] = {
|
||||||
|
'address': summaryData['address'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger().d('Registration data prepared and ready for submission');
|
||||||
|
|
||||||
|
// Submit to user repository
|
||||||
|
final userRepo = Get.find<UserRepository>();
|
||||||
|
final result = await userRepo.updateUserProfile(finalData);
|
||||||
|
|
||||||
|
Logger().d('Registration submission result: $result');
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error saving registration data: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,15 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart';
|
|
||||||
import 'package:sigap/src/features/auth/data/bindings/registration_binding.dart';
|
|
||||||
import 'package:sigap/src/features/auth/data/models/face_model.dart';
|
import 'package:sigap/src/features/auth/data/models/face_model.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/ktp_model.dart';
|
import 'package:sigap/src/features/auth/data/models/ktp_model.dart';
|
||||||
import 'package:sigap/src/features/auth/data/services/registration_service.dart';
|
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/basic/signup_with_role_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/id-card-verification/id_card_verification_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/facial_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/selfie-verification/selfie_verification_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/viewer-information/personal_info_controller.dart';
|
||||||
|
|
||||||
class IdentityVerificationController extends GetxController {
|
class IdentityVerificationController extends GetxController {
|
||||||
// Singleton instance
|
// Singleton instance
|
||||||
|
@ -21,23 +21,22 @@ class IdentityVerificationController extends GetxController {
|
||||||
// Dependencies
|
// Dependencies
|
||||||
final bool isOfficer;
|
final bool isOfficer;
|
||||||
final AzureOCRService _ocrService = AzureOCRService();
|
final AzureOCRService _ocrService = AzureOCRService();
|
||||||
// Use FacialVerificationService instead of direct EdgeFunction
|
|
||||||
final FacialVerificationService _faceService =
|
final FacialVerificationService _faceService =
|
||||||
FacialVerificationService.instance;
|
FacialVerificationService.instance;
|
||||||
|
|
||||||
// Local storage keys (matching those in IdCardVerificationController)
|
// Local storage keys
|
||||||
static const String _kOcrResultsKey = 'ocr_results';
|
static const String _kOcrResultsKey = 'ocr_results';
|
||||||
static const String _kOcrModelKey = 'ocr_model';
|
static const String _kOcrModelKey = 'ocr_model';
|
||||||
static const String _kIdCardTypeKey = 'id_card_type';
|
static const String _kIdCardTypeKey = 'id_card_type';
|
||||||
|
|
||||||
// Controllers
|
// Controllers for form fields
|
||||||
final TextEditingController nikController = TextEditingController();
|
final TextEditingController nikController = TextEditingController();
|
||||||
final TextEditingController fullNameController = TextEditingController();
|
final TextEditingController fullNameController = TextEditingController();
|
||||||
final TextEditingController placeOfBirthController = TextEditingController();
|
final TextEditingController placeOfBirthController = TextEditingController();
|
||||||
final TextEditingController birthDateController = TextEditingController();
|
final TextEditingController birthDateController = TextEditingController();
|
||||||
final TextEditingController addressController = TextEditingController();
|
final TextEditingController addressController = TextEditingController();
|
||||||
|
|
||||||
// Error variables
|
// Form validation errors
|
||||||
final RxString nikError = RxString('');
|
final RxString nikError = RxString('');
|
||||||
final RxString fullNameError = RxString('');
|
final RxString fullNameError = RxString('');
|
||||||
final RxString placeOfBirthError = RxString('');
|
final RxString placeOfBirthError = RxString('');
|
||||||
|
@ -45,45 +44,51 @@ class IdentityVerificationController extends GetxController {
|
||||||
final RxString genderError = RxString('');
|
final RxString genderError = RxString('');
|
||||||
final RxString addressError = RxString('');
|
final RxString addressError = RxString('');
|
||||||
|
|
||||||
// Verification states
|
// ID verification states
|
||||||
final RxBool isVerifying = RxBool(false);
|
final RxBool isVerifying = RxBool(false);
|
||||||
final RxBool isVerified = RxBool(false);
|
final RxBool isVerified = RxBool(false);
|
||||||
final RxString verificationMessage = RxString('');
|
final RxString verificationMessage = RxString('');
|
||||||
|
|
||||||
// Face verification
|
// Face verification states
|
||||||
final RxBool isVerifyingFace = RxBool(false);
|
final RxBool isVerifyingFace = RxBool(false);
|
||||||
final RxBool isFaceVerified = RxBool(false);
|
final RxBool isFaceVerified = RxBool(false);
|
||||||
final RxString faceVerificationMessage = RxString('');
|
final RxString faceVerificationMessage = RxString('');
|
||||||
|
|
||||||
// Use FaceComparisonResult for face verification
|
|
||||||
final Rx<FaceComparisonResult?> faceComparisonResult =
|
final Rx<FaceComparisonResult?> faceComparisonResult =
|
||||||
Rx<FaceComparisonResult?>(null);
|
Rx<FaceComparisonResult?>(null);
|
||||||
|
|
||||||
// Gender selection - initialize with a default value
|
// Gender selection dropdown
|
||||||
final Rx<String?> selectedGender = Rx<String?>('Male');
|
final Rx<String?> selectedGender = Rx<String?>('Male');
|
||||||
|
|
||||||
// Form validation
|
// Form validation state
|
||||||
final RxBool isFormValid = RxBool(true);
|
final RxBool isFormValid = RxBool(true);
|
||||||
|
|
||||||
// Flag to prevent infinite loop
|
// Flag to prevent infinite loop
|
||||||
bool _isApplyingData = false;
|
bool _isApplyingData = false;
|
||||||
|
|
||||||
// NIK field readonly status
|
// UI control states
|
||||||
final RxBool isNikReadOnly = RxBool(false);
|
final RxBool isNikReadOnly = RxBool(false);
|
||||||
|
|
||||||
// Properties to store extracted ID card data
|
|
||||||
final String? extractedIdCardNumber;
|
|
||||||
final String? extractedName;
|
|
||||||
final RxBool isPreFilledNik = false.obs;
|
final RxBool isPreFilledNik = false.obs;
|
||||||
|
|
||||||
// Store the loaded OCR data
|
// Storage for extracted data
|
||||||
final RxMap<String, dynamic> ocrData = RxMap<String, dynamic>({});
|
final RxMap<String, dynamic> ocrData = RxMap<String, dynamic>({});
|
||||||
|
final String? extractedIdCardNumber;
|
||||||
|
final String? extractedName;
|
||||||
|
|
||||||
// Status of data saving
|
// Data saving states
|
||||||
final RxBool isSavingData = RxBool(false);
|
final RxBool isSavingData = RxBool(false);
|
||||||
final RxBool isDataSaved = RxBool(false);
|
final RxBool isDataSaved = RxBool(false);
|
||||||
final RxString dataSaveMessage = RxString('');
|
final RxString dataSaveMessage = RxString('');
|
||||||
|
|
||||||
|
// Summary data for review page
|
||||||
|
final RxMap<String, dynamic> summaryData = RxMap<String, dynamic>({});
|
||||||
|
|
||||||
|
// Verification status of different sections
|
||||||
|
final RxBool isBasicInfoVerified = RxBool(false);
|
||||||
|
final RxBool isIdCardVerified = RxBool(false);
|
||||||
|
final RxBool isSelfieVerified = RxBool(false);
|
||||||
|
final RxBool isContactInfoVerified = RxBool(false);
|
||||||
|
final RxBool isLoadingSummary = RxBool(false);
|
||||||
|
|
||||||
IdentityVerificationController({
|
IdentityVerificationController({
|
||||||
this.extractedIdCardNumber = '',
|
this.extractedIdCardNumber = '',
|
||||||
this.extractedName = '',
|
this.extractedName = '',
|
||||||
|
@ -93,14 +98,173 @@ class IdentityVerificationController extends GetxController {
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
// Make sure selectedGender has a default value
|
// Set default gender value
|
||||||
selectedGender.value = selectedGender.value ?? 'Male';
|
selectedGender.value = selectedGender.value ?? 'Male';
|
||||||
|
|
||||||
// Load OCR data from local storage with debug info
|
// Load data in sequence
|
||||||
print(
|
_initializeData();
|
||||||
'Initializing IdentityVerificationController and loading OCR data...',
|
}
|
||||||
);
|
|
||||||
loadOcrDataFromLocalStorage();
|
// Initialize all data in sequence
|
||||||
|
Future<void> _initializeData() async {
|
||||||
|
try {
|
||||||
|
// First load OCR data
|
||||||
|
await loadOcrDataFromLocalStorage();
|
||||||
|
|
||||||
|
// Then load data from previous steps for summary
|
||||||
|
await loadAllStepsData();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error initializing data: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data from all previous steps
|
||||||
|
Future<void> loadAllStepsData() async {
|
||||||
|
try {
|
||||||
|
isLoadingSummary.value = true;
|
||||||
|
|
||||||
|
// Get references to all controllers
|
||||||
|
final registrationController = Get.find<FormRegistrationController>();
|
||||||
|
final idCardController = Get.find<IdCardVerificationController>();
|
||||||
|
final selfieController = Get.find<SelfieVerificationController>();
|
||||||
|
|
||||||
|
// Load data from each controller
|
||||||
|
_loadBasicInfoData(registrationController);
|
||||||
|
_loadIdCardData(idCardController);
|
||||||
|
_loadSelfieData(selfieController);
|
||||||
|
|
||||||
|
// Pre-fill form with extracted data
|
||||||
|
_prefillFormWithExtractedData(idCardController);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading steps data: $e');
|
||||||
|
} finally {
|
||||||
|
isLoadingSummary.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load basic information
|
||||||
|
void _loadBasicInfoData(FormRegistrationController controller) {
|
||||||
|
try {
|
||||||
|
// Add basic info to summary
|
||||||
|
summaryData['email'] = SignupWithRoleController().emailController.text;
|
||||||
|
summaryData['phone'] = PersonalInfoController.instance.phoneController.text;
|
||||||
|
summaryData['role'] = controller.selectedRole.value?.name ?? 'Unknown';
|
||||||
|
|
||||||
|
isBasicInfoVerified.value = true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading basic info: $e');
|
||||||
|
isBasicInfoVerified.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load ID card data
|
||||||
|
void _loadIdCardData(IdCardVerificationController controller) {
|
||||||
|
try {
|
||||||
|
// Add ID card info to summary
|
||||||
|
summaryData['idCardType'] = isOfficer ? 'KTA' : 'KTP';
|
||||||
|
summaryData['idCardValid'] = controller.isIdCardValid.value;
|
||||||
|
summaryData['idCardConfirmed'] = controller.hasConfirmedIdCard.value;
|
||||||
|
summaryData['extractedInfo'] = controller.extractedInfo;
|
||||||
|
|
||||||
|
// Add model data
|
||||||
|
if (isOfficer) {
|
||||||
|
summaryData['ktaModel'] = controller.ktaModel.value?.toJson();
|
||||||
|
} else {
|
||||||
|
summaryData['ktpModel'] = controller.ktpModel.value?.toJson();
|
||||||
|
}
|
||||||
|
|
||||||
|
isIdCardVerified.value =
|
||||||
|
controller.isIdCardValid.value && controller.hasConfirmedIdCard.value;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading ID card data: $e');
|
||||||
|
isIdCardVerified.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load selfie data
|
||||||
|
void _loadSelfieData(SelfieVerificationController controller) {
|
||||||
|
try {
|
||||||
|
// Add selfie verification info to summary
|
||||||
|
summaryData['selfieValid'] = controller.isSelfieValid.value;
|
||||||
|
summaryData['selfieConfirmed'] = controller.hasConfirmedSelfie.value;
|
||||||
|
summaryData['livenessCheckPassed'] =
|
||||||
|
controller.isLivenessCheckPassed.value;
|
||||||
|
summaryData['faceMatchResult'] = controller.isMatchWithIDCard.value;
|
||||||
|
summaryData['faceMatchConfidence'] = controller.matchConfidence.value;
|
||||||
|
|
||||||
|
isSelfieVerified.value =
|
||||||
|
controller.isSelfieValid.value && controller.hasConfirmedSelfie.value;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error loading selfie data: $e');
|
||||||
|
isSelfieVerified.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-fill form with extracted data
|
||||||
|
void _prefillFormWithExtractedData(IdCardVerificationController controller) {
|
||||||
|
try {
|
||||||
|
if (!isOfficer && controller.ktpModel.value != null) {
|
||||||
|
// Extract KTP data
|
||||||
|
final ktp = controller.ktpModel.value!;
|
||||||
|
|
||||||
|
if (ktp.nik.isNotEmpty) {
|
||||||
|
nikController.text = ktp.nik;
|
||||||
|
summaryData['nik'] = ktp.nik;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ktp.name.isNotEmpty) {
|
||||||
|
fullNameController.text = ktp.name;
|
||||||
|
summaryData['fullName'] = ktp.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ktp.birthPlace.isNotEmpty) {
|
||||||
|
placeOfBirthController.text = ktp.birthPlace;
|
||||||
|
summaryData['placeOfBirth'] = ktp.birthPlace;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ktp.birthDate.isNotEmpty) {
|
||||||
|
birthDateController.text = ktp.birthDate;
|
||||||
|
summaryData['birthDate'] = ktp.birthDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ktp.gender.isNotEmpty) {
|
||||||
|
// Convert gender to the format expected by the dropdown
|
||||||
|
String gender = ktp.gender.toLowerCase();
|
||||||
|
if (gender.contains('laki') || gender == 'male') {
|
||||||
|
selectedGender.value = 'Male';
|
||||||
|
} else if (gender.contains('perempuan') || gender == 'female') {
|
||||||
|
selectedGender.value = 'Female';
|
||||||
|
}
|
||||||
|
summaryData['gender'] = selectedGender.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ktp.address.isNotEmpty) {
|
||||||
|
addressController.text = ktp.address;
|
||||||
|
summaryData['address'] = ktp.address;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make NIK field read-only since it's extracted from KTP
|
||||||
|
isNikReadOnly.value = true;
|
||||||
|
} else if (isOfficer && controller.ktaModel.value != null) {
|
||||||
|
// Extract KTA data
|
||||||
|
final kta = controller.ktaModel.value!;
|
||||||
|
|
||||||
|
if (kta.name.isNotEmpty) {
|
||||||
|
fullNameController.text = kta.name;
|
||||||
|
summaryData['fullName'] = kta.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract birth date from extra data if available
|
||||||
|
if (kta.extraData != null &&
|
||||||
|
kta.extraData!.containsKey('tanggal_lahir') &&
|
||||||
|
kta.extraData!['tanggal_lahir'] != null) {
|
||||||
|
birthDateController.text = kta.extraData!['tanggal_lahir'];
|
||||||
|
summaryData['birthDate'] = kta.extraData!['tanggal_lahir'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error prefilling form with extracted data: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load OCR data from local storage
|
// Load OCR data from local storage
|
||||||
|
@ -108,7 +272,6 @@ class IdentityVerificationController extends GetxController {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
// Load stored ID card type to verify it matches current flow
|
|
||||||
final String? storedIdCardType = prefs.getString(_kIdCardTypeKey);
|
final String? storedIdCardType = prefs.getString(_kIdCardTypeKey);
|
||||||
print(
|
print(
|
||||||
'Stored ID card type: $storedIdCardType, Current isOfficer: $isOfficer',
|
'Stored ID card type: $storedIdCardType, Current isOfficer: $isOfficer',
|
||||||
|
@ -121,7 +284,6 @@ class IdentityVerificationController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load OCR results
|
|
||||||
final String? jsonData = prefs.getString(_kOcrResultsKey);
|
final String? jsonData = prefs.getString(_kOcrResultsKey);
|
||||||
if (jsonData != null) {
|
if (jsonData != null) {
|
||||||
print('Found OCR data in storage: ${jsonData.length} chars');
|
print('Found OCR data in storage: ${jsonData.length} chars');
|
||||||
|
@ -129,11 +291,8 @@ class IdentityVerificationController extends GetxController {
|
||||||
ocrData.assignAll(results);
|
ocrData.assignAll(results);
|
||||||
print('OCR data loaded: ${results.length} items');
|
print('OCR data loaded: ${results.length} items');
|
||||||
|
|
||||||
// Load OCR model
|
|
||||||
final String? modelJson = prefs.getString(_kOcrModelKey);
|
final String? modelJson = prefs.getString(_kOcrModelKey);
|
||||||
if (modelJson != null) {
|
if (modelJson != null) {
|
||||||
print('Found OCR model in storage: ${modelJson.length} chars');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isOfficer) {
|
if (isOfficer) {
|
||||||
final ktaModel = KtaModel.fromJson(jsonDecode(modelJson));
|
final ktaModel = KtaModel.fromJson(jsonDecode(modelJson));
|
||||||
|
@ -145,7 +304,6 @@ class IdentityVerificationController extends GetxController {
|
||||||
applyKtpDataToForm(ktpModel);
|
applyKtpDataToForm(ktpModel);
|
||||||
}
|
}
|
||||||
isNikReadOnly.value = true;
|
isNikReadOnly.value = true;
|
||||||
print('NIK field set to read-only');
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error parsing model JSON: $e');
|
print('Error parsing model JSON: $e');
|
||||||
}
|
}
|
||||||
|
@ -156,7 +314,6 @@ class IdentityVerificationController extends GetxController {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading OCR data from local storage: $e');
|
print('Error loading OCR data from local storage: $e');
|
||||||
} finally {
|
} finally {
|
||||||
// If data wasn't loaded from local storage, try from FormRegistrationController
|
|
||||||
if (ocrData.isEmpty) {
|
if (ocrData.isEmpty) {
|
||||||
print('Falling back to FormRegistrationController data');
|
print('Falling back to FormRegistrationController data');
|
||||||
_safeApplyIdCardData();
|
_safeApplyIdCardData();
|
||||||
|
@ -166,24 +323,14 @@ class IdentityVerificationController extends GetxController {
|
||||||
|
|
||||||
// Apply KTP data to form
|
// Apply KTP data to form
|
||||||
void applyKtpDataToForm(KtpModel ktpModel) {
|
void applyKtpDataToForm(KtpModel ktpModel) {
|
||||||
if (ktpModel.nik.isNotEmpty) {
|
if (ktpModel.nik.isNotEmpty) nikController.text = ktpModel.nik;
|
||||||
nikController.text = ktpModel.nik;
|
if (ktpModel.name.isNotEmpty) fullNameController.text = ktpModel.name;
|
||||||
}
|
if (ktpModel.birthPlace.isNotEmpty)
|
||||||
|
|
||||||
if (ktpModel.name.isNotEmpty) {
|
|
||||||
fullNameController.text = ktpModel.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ktpModel.birthPlace.isNotEmpty) {
|
|
||||||
placeOfBirthController.text = ktpModel.birthPlace;
|
placeOfBirthController.text = ktpModel.birthPlace;
|
||||||
}
|
if (ktpModel.birthDate.isNotEmpty)
|
||||||
|
|
||||||
if (ktpModel.birthDate.isNotEmpty) {
|
|
||||||
birthDateController.text = ktpModel.birthDate;
|
birthDateController.text = ktpModel.birthDate;
|
||||||
}
|
|
||||||
|
|
||||||
if (ktpModel.gender.isNotEmpty) {
|
if (ktpModel.gender.isNotEmpty) {
|
||||||
// Convert gender to the format expected by the dropdown
|
|
||||||
String gender = ktpModel.gender.toLowerCase();
|
String gender = ktpModel.gender.toLowerCase();
|
||||||
if (gender.contains('laki') || gender == 'male') {
|
if (gender.contains('laki') || gender == 'male') {
|
||||||
selectedGender.value = 'Male';
|
selectedGender.value = 'Male';
|
||||||
|
@ -191,56 +338,42 @@ class IdentityVerificationController extends GetxController {
|
||||||
selectedGender.value = 'Female';
|
selectedGender.value = 'Female';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ktpModel.address.isNotEmpty) {
|
if (ktpModel.address.isNotEmpty) addressController.text = ktpModel.address;
|
||||||
addressController.text = ktpModel.address;
|
|
||||||
}
|
// Mark as verified
|
||||||
|
|
||||||
// Mark as verified since we have validated KTP data
|
|
||||||
isVerified.value = true;
|
isVerified.value = true;
|
||||||
verificationMessage.value = 'KTP information loaded successfully';
|
verificationMessage.value = 'KTP information loaded successfully';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply KTA data to form
|
// Apply KTA data to form
|
||||||
void applyKtaDataToForm(KtaModel ktaModel) {
|
void applyKtaDataToForm(KtaModel ktaModel) {
|
||||||
// For officer, we'd fill in different fields as needed
|
if (ktaModel.name.isNotEmpty) fullNameController.text = ktaModel.name;
|
||||||
if (ktaModel.name.isNotEmpty) {
|
|
||||||
fullNameController.text = ktaModel.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If birthDate is available in extra data
|
|
||||||
if (ktaModel.extraData != null &&
|
if (ktaModel.extraData != null &&
|
||||||
ktaModel.extraData!.containsKey('tanggal_lahir') &&
|
ktaModel.extraData!.containsKey('tanggal_lahir') &&
|
||||||
ktaModel.extraData!['tanggal_lahir'] != null) {
|
ktaModel.extraData!['tanggal_lahir'] != null) {
|
||||||
birthDateController.text = ktaModel.extraData!['tanggal_lahir'];
|
birthDateController.text = ktaModel.extraData!['tanggal_lahir'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as verified
|
|
||||||
isVerified.value = true;
|
isVerified.value = true;
|
||||||
verificationMessage.value = 'KTA information loaded successfully';
|
verificationMessage.value = 'KTA information loaded successfully';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safely apply ID card data without risking stack overflow (fallback method)
|
// Safe method to apply ID card data without risk of stack overflow
|
||||||
void _safeApplyIdCardData() {
|
void _safeApplyIdCardData() {
|
||||||
if (_isApplyingData) return; // Guard against recursive calls
|
if (_isApplyingData) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_isApplyingData = true;
|
_isApplyingData = true;
|
||||||
|
|
||||||
// Check if FormRegistrationController is ready
|
if (!Get.isRegistered<FormRegistrationController>()) return;
|
||||||
if (!Get.isRegistered<FormRegistrationController>()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final formController = Get.find<FormRegistrationController>();
|
final formController = Get.find<FormRegistrationController>();
|
||||||
if (formController.idCardData.value == null) {
|
if (formController.idCardData.value == null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final idCardData = formController.idCardData.value;
|
final idCardData = formController.idCardData.value;
|
||||||
|
|
||||||
if (idCardData != null) {
|
if (idCardData != null) {
|
||||||
// Fill the form with the extracted data
|
|
||||||
if (!isOfficer && idCardData is KtpModel) {
|
if (!isOfficer && idCardData is KtpModel) {
|
||||||
applyKtpDataToForm(idCardData);
|
applyKtpDataToForm(idCardData);
|
||||||
isNikReadOnly.value = true;
|
isNikReadOnly.value = true;
|
||||||
|
@ -256,11 +389,13 @@ class IdentityVerificationController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate form inputs
|
// Validate form inputs
|
||||||
bool validate(GlobalKey<FormState> formKey) {
|
bool validate(GlobalKey<FormState>? formKey) {
|
||||||
isFormValid.value = true;
|
isFormValid.value = true;
|
||||||
|
clearErrors();
|
||||||
|
|
||||||
// For non-officers, we need to validate NIK and other KTP-related fields
|
// Validate required fields based on officer status
|
||||||
if (!isOfficer) {
|
if (!isOfficer) {
|
||||||
|
// KTP validation
|
||||||
if (nikController.text.isEmpty) {
|
if (nikController.text.isEmpty) {
|
||||||
nikError.value = 'NIK is required';
|
nikError.value = 'NIK is required';
|
||||||
isFormValid.value = false;
|
isFormValid.value = false;
|
||||||
|
@ -278,37 +413,64 @@ class IdentityVerificationController extends GetxController {
|
||||||
placeOfBirthError.value = 'Place of birth is required';
|
placeOfBirthError.value = 'Place of birth is required';
|
||||||
isFormValid.value = false;
|
isFormValid.value = false;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// KTA validation
|
||||||
|
if (fullNameController.text.isEmpty) {
|
||||||
|
fullNameError.value = 'Full name is required';
|
||||||
|
isFormValid.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// These validations apply to both officers and non-officers
|
// Common validations
|
||||||
if (birthDateController.text.isEmpty) {
|
if (birthDateController.text.isEmpty) {
|
||||||
birthDateError.value = 'Birth date is required';
|
birthDateError.value = 'Birth date is required';
|
||||||
isFormValid.value = false;
|
isFormValid.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (addressController.text.isEmpty) {
|
if (selectedGender.value == null) {
|
||||||
// addressError.value = 'Address is required';
|
genderError.value = 'Gender is required';
|
||||||
// isFormValid.value = false;
|
isFormValid.value = false;
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
// Verify previous steps completion
|
||||||
|
bool allPreviousStepsCompleted =
|
||||||
|
isBasicInfoVerified.value &&
|
||||||
|
isIdCardVerified.value &&
|
||||||
|
isSelfieVerified.value;
|
||||||
|
|
||||||
|
if (!allPreviousStepsCompleted) {
|
||||||
|
isFormValid.value = false;
|
||||||
|
verificationMessage.value =
|
||||||
|
'Please complete all previous steps before submitting';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update summary data
|
||||||
|
_updateSummaryWithFormData();
|
||||||
|
|
||||||
return isFormValid.value;
|
return isFormValid.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update summary with current form data
|
||||||
|
void _updateSummaryWithFormData() {
|
||||||
|
summaryData['nik'] = nikController.text;
|
||||||
|
summaryData['fullName'] = fullNameController.text;
|
||||||
|
summaryData['placeOfBirth'] = placeOfBirthController.text;
|
||||||
|
summaryData['birthDate'] = birthDateController.text;
|
||||||
|
summaryData['gender'] = selectedGender.value;
|
||||||
|
summaryData['address'] = addressController.text;
|
||||||
|
}
|
||||||
|
|
||||||
// Verify ID card information against OCR results
|
// Verify ID card with OCR data
|
||||||
void verifyIdCardWithOCR() {
|
void verifyIdCardWithOCR() {
|
||||||
try {
|
try {
|
||||||
isVerifying.value = true;
|
isVerifying.value = true;
|
||||||
|
|
||||||
// Compare form input with OCR results
|
|
||||||
final formController = Get.find<FormRegistrationController>();
|
final formController = Get.find<FormRegistrationController>();
|
||||||
final idCardData = formController.idCardData.value;
|
final idCardData = formController.idCardData.value;
|
||||||
|
|
||||||
if (idCardData != null) {
|
if (idCardData != null) {
|
||||||
if (!isOfficer && idCardData is KtpModel) {
|
if (!isOfficer && idCardData is KtpModel) {
|
||||||
// Verify NIK matches
|
|
||||||
bool nikMatches = nikController.text == idCardData.nik;
|
bool nikMatches = nikController.text == idCardData.nik;
|
||||||
|
|
||||||
// Verify name is similar (accounting for slight differences in formatting)
|
|
||||||
bool nameMatches = _compareNames(
|
bool nameMatches = _compareNames(
|
||||||
fullNameController.text,
|
fullNameController.text,
|
||||||
idCardData.name,
|
idCardData.name,
|
||||||
|
@ -324,7 +486,6 @@ class IdentityVerificationController extends GetxController {
|
||||||
'Information doesn\'t match with KTP. Please check and try again.';
|
'Information doesn\'t match with KTP. Please check and try again.';
|
||||||
}
|
}
|
||||||
} else if (isOfficer && idCardData is KtaModel) {
|
} else if (isOfficer && idCardData is KtaModel) {
|
||||||
// For officers, verify that the name matches
|
|
||||||
bool nameMatches = _compareNames(
|
bool nameMatches = _compareNames(
|
||||||
fullNameController.text,
|
fullNameController.text,
|
||||||
idCardData.name,
|
idCardData.name,
|
||||||
|
@ -354,9 +515,8 @@ class IdentityVerificationController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple name comparison function (ignores case, spaces)
|
// Compare names accounting for formatting differences
|
||||||
bool _compareNames(String name1, String name2) {
|
bool _compareNames(String name1, String name2) {
|
||||||
// Normalize names for comparison
|
|
||||||
String normalizedName1 = name1.toLowerCase().trim().replaceAll(
|
String normalizedName1 = name1.toLowerCase().trim().replaceAll(
|
||||||
RegExp(r'\s+'),
|
RegExp(r'\s+'),
|
||||||
' ',
|
' ',
|
||||||
|
@ -366,24 +526,20 @@ class IdentityVerificationController extends GetxController {
|
||||||
' ',
|
' ',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check exact match
|
|
||||||
if (normalizedName1 == normalizedName2) return true;
|
if (normalizedName1 == normalizedName2) return true;
|
||||||
|
|
||||||
// Check if one name is contained within the other
|
|
||||||
if (normalizedName1.contains(normalizedName2) ||
|
if (normalizedName1.contains(normalizedName2) ||
|
||||||
normalizedName2.contains(normalizedName1))
|
normalizedName2.contains(normalizedName1))
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
// Split names into parts and check for partial matches
|
|
||||||
var parts1 = normalizedName1.split(' ');
|
var parts1 = normalizedName1.split(' ');
|
||||||
var parts2 = normalizedName2.split(' ');
|
var parts2 = normalizedName2.split(' ');
|
||||||
|
|
||||||
// Count matching name parts
|
|
||||||
int matches = 0;
|
int matches = 0;
|
||||||
for (var part1 in parts1) {
|
for (var part1 in parts1) {
|
||||||
for (var part2 in parts2) {
|
for (var part2 in parts2) {
|
||||||
if (part1.length > 2 &&
|
if (part1.length > 2 &&
|
||||||
part2.length > 2 &&
|
part2.length > 2 &&
|
||||||
(part1.contains(part2) || part2.contains(part1))) {
|
(part1.contains(part2) || part2.contains(part1))) {
|
||||||
matches++;
|
matches++;
|
||||||
break;
|
break;
|
||||||
|
@ -391,25 +547,22 @@ class IdentityVerificationController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If more than half of the name parts match, consider it a match
|
|
||||||
return matches >= (parts1.length / 2).floor();
|
return matches >= (parts1.length / 2).floor();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Face verification function using EdgeFunction instead of AWS directly
|
// Verify face match using FacialVerificationService
|
||||||
void verifyFaceMatch() {
|
void verifyFaceMatch() {
|
||||||
// Set quick verification status for development
|
|
||||||
if (_faceService.skipFaceVerification) {
|
if (_faceService.skipFaceVerification) {
|
||||||
|
// Development mode - use dummy data
|
||||||
isFaceVerified.value = true;
|
isFaceVerified.value = true;
|
||||||
faceVerificationMessage.value =
|
faceVerificationMessage.value =
|
||||||
'Face verification skipped (development mode)';
|
'Face verification skipped (development mode)';
|
||||||
|
|
||||||
// Create dummy comparison result
|
|
||||||
final idCardController = Get.find<IdCardVerificationController>();
|
final idCardController = Get.find<IdCardVerificationController>();
|
||||||
final selfieController = Get.find<SelfieVerificationController>();
|
final selfieController = Get.find<SelfieVerificationController>();
|
||||||
|
|
||||||
if (idCardController.idCardImage.value != null &&
|
if (idCardController.idCardImage.value != null &&
|
||||||
selfieController.selfieImage.value != null) {
|
selfieController.selfieImage.value != null) {
|
||||||
// Set dummy result
|
|
||||||
faceComparisonResult.value = FaceComparisonResult(
|
faceComparisonResult.value = FaceComparisonResult(
|
||||||
sourceFace: FaceModel(
|
sourceFace: FaceModel(
|
||||||
imagePath: idCardController.idCardImage.value!.path,
|
imagePath: idCardController.idCardImage.value!.path,
|
||||||
|
@ -428,38 +581,30 @@ class IdentityVerificationController extends GetxController {
|
||||||
message: 'Face verification passed (development mode)',
|
message: 'Face verification passed (development mode)',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isVerifyingFace.value = true;
|
isVerifyingFace.value = true;
|
||||||
|
|
||||||
// Get ID card and selfie images
|
|
||||||
final formController = Get.find<FormRegistrationController>();
|
|
||||||
final idCardController = Get.find<IdCardVerificationController>();
|
final idCardController = Get.find<IdCardVerificationController>();
|
||||||
final selfieController = Get.find<SelfieVerificationController>();
|
final selfieController = Get.find<SelfieVerificationController>();
|
||||||
|
|
||||||
// Check if we have both images
|
|
||||||
if (idCardController.idCardImage.value == null ||
|
if (idCardController.idCardImage.value == null ||
|
||||||
selfieController.selfieImage.value == null) {
|
selfieController.selfieImage.value == null) {
|
||||||
isFaceVerified.value = false;
|
isFaceVerified.value = false;
|
||||||
faceVerificationMessage.value =
|
faceVerificationMessage.value =
|
||||||
'Both ID card and selfie are required for face verification.';
|
'Both ID card and selfie are required for face verification.';
|
||||||
isVerifyingFace.value = false;
|
isVerifyingFace.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use FacialVerificationService to compare faces
|
|
||||||
_faceService
|
_faceService
|
||||||
.compareFaces(
|
.compareFaces(
|
||||||
idCardController.idCardImage.value!,
|
idCardController.idCardImage.value!,
|
||||||
selfieController.selfieImage.value!,
|
selfieController.selfieImage.value!,
|
||||||
)
|
)
|
||||||
.then((result) {
|
.then((result) {
|
||||||
// Store the comparison result
|
|
||||||
faceComparisonResult.value = result;
|
faceComparisonResult.value = result;
|
||||||
|
|
||||||
// Update verification status
|
|
||||||
isFaceVerified.value = result.isMatch;
|
isFaceVerified.value = result.isMatch;
|
||||||
faceVerificationMessage.value = result.message;
|
faceVerificationMessage.value = result.message;
|
||||||
})
|
})
|
||||||
|
@ -473,7 +618,7 @@ class IdentityVerificationController extends GetxController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear all error messages
|
// Clear all validation errors
|
||||||
void clearErrors() {
|
void clearErrors() {
|
||||||
nikError.value = '';
|
nikError.value = '';
|
||||||
fullNameError.value = '';
|
fullNameError.value = '';
|
||||||
|
@ -481,21 +626,10 @@ class IdentityVerificationController extends GetxController {
|
||||||
birthDateError.value = '';
|
birthDateError.value = '';
|
||||||
genderError.value = '';
|
genderError.value = '';
|
||||||
addressError.value = '';
|
addressError.value = '';
|
||||||
|
|
||||||
isFormValid.value = true;
|
isFormValid.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
// Prefill form with extracted data
|
||||||
void onClose() {
|
|
||||||
nikController.dispose();
|
|
||||||
fullNameController.dispose();
|
|
||||||
placeOfBirthController.dispose();
|
|
||||||
birthDateController.dispose();
|
|
||||||
addressController.dispose();
|
|
||||||
super.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method to pre-fill NIK and Name from the extracted data
|
|
||||||
void prefillExtractedData() {
|
void prefillExtractedData() {
|
||||||
if (extractedIdCardNumber != null && extractedIdCardNumber!.isNotEmpty) {
|
if (extractedIdCardNumber != null && extractedIdCardNumber!.isNotEmpty) {
|
||||||
nikController.text = extractedIdCardNumber!;
|
nikController.text = extractedIdCardNumber!;
|
||||||
|
@ -513,14 +647,39 @@ class IdentityVerificationController extends GetxController {
|
||||||
try {
|
try {
|
||||||
isSavingData.value = true;
|
isSavingData.value = true;
|
||||||
dataSaveMessage.value = 'Saving your registration data...';
|
dataSaveMessage.value = 'Saving your registration data...';
|
||||||
|
|
||||||
// Ensure the registration service is available();
|
// Final validation
|
||||||
if (!Get.isRegistered<RegistrationService>()) {
|
if (!validate(null)) {
|
||||||
await Get.putAsync(() async => RegistrationService());
|
dataSaveMessage.value = 'Please fix the errors before submitting';
|
||||||
} // Get registration service
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
final registrationService = Get.find<RegistrationService>();
|
// Update summary with final data
|
||||||
final result = await registrationService.saveRegistrationData();
|
_updateSummaryWithFormData();
|
||||||
|
|
||||||
|
// Format data according to models
|
||||||
|
Map<String, dynamic> formattedData = {
|
||||||
|
// Match format from summaryData to match ProfileModel and OfficerModel
|
||||||
|
'nik': nikController.text,
|
||||||
|
'fullName': fullNameController.text,
|
||||||
|
'placeOfBirth': placeOfBirthController.text,
|
||||||
|
'birthDate': birthDateController.text,
|
||||||
|
'gender': selectedGender.value,
|
||||||
|
'address': addressController.text,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add all other summary data for completeness
|
||||||
|
summaryData.forEach((key, value) {
|
||||||
|
if (!formattedData.containsKey(key)) {
|
||||||
|
formattedData[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use FormRegistrationController for actual submission
|
||||||
|
final formController = Get.find<FormRegistrationController>();
|
||||||
|
final result = await formController.saveRegistrationData(
|
||||||
|
summaryData: formattedData,
|
||||||
|
);
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
isDataSaved.value = true;
|
isDataSaved.value = true;
|
||||||
|
@ -541,4 +700,15 @@ class IdentityVerificationController extends GetxController {
|
||||||
isSavingData.value = false;
|
isSavingData.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
// Dispose controllers to prevent memory leaks
|
||||||
|
nikController.dispose();
|
||||||
|
fullNameController.dispose();
|
||||||
|
placeOfBirthController.dispose();
|
||||||
|
birthDateController.dispose();
|
||||||
|
addressController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/basic/registration_form_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/identity-verification/identity_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/id_info_form.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/identity_verification/id_info_form.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/widgets/verification_summary.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/utils/constants/colors.dart';
|
||||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
|
||||||
class IdentityVerificationStep extends StatelessWidget {
|
class IdentityVerificationStep extends StatelessWidget {
|
||||||
|
@ -15,7 +17,7 @@ class IdentityVerificationStep extends StatelessWidget {
|
||||||
final controller = Get.find<IdentityVerificationController>();
|
final controller = Get.find<IdentityVerificationController>();
|
||||||
final mainController = Get.find<FormRegistrationController>();
|
final mainController = Get.find<FormRegistrationController>();
|
||||||
mainController.formKey = formKey;
|
mainController.formKey = formKey;
|
||||||
|
|
||||||
final isOfficer = mainController.selectedRole.value?.isOfficer ?? false;
|
final isOfficer = mainController.selectedRole.value?.isOfficer ?? false;
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
|
@ -23,21 +25,305 @@ class IdentityVerificationStep extends StatelessWidget {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Section Header
|
||||||
FormSectionHeader(
|
FormSectionHeader(
|
||||||
title: 'Additional Information',
|
title: 'Review & Verification',
|
||||||
subtitle: isOfficer
|
subtitle:
|
||||||
? 'Please provide additional personal details'
|
'Please review and confirm your information before submitting',
|
||||||
: 'Please verify your KTP information below. NIK field cannot be edited.',
|
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
// Personal Information Form Section
|
// Verification Progress Card
|
||||||
|
Obx(() => _buildVerificationProgressCard(controller)),
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
|
// Registration Summary
|
||||||
|
Obx(
|
||||||
|
() => VerificationSummary(
|
||||||
|
summaryData: controller.summaryData,
|
||||||
|
isOfficer: isOfficer,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
|
// Form section header
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: TSizes.spaceBtwItems,
|
||||||
|
bottom: TSizes.sm,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: Colors.grey.withOpacity(0.2), width: 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: FormSectionHeader(
|
||||||
|
title: 'Confirm Identity Information',
|
||||||
|
subtitle:
|
||||||
|
isOfficer
|
||||||
|
? 'Please verify the pre-filled information from your KTA'
|
||||||
|
: 'Please verify the pre-filled information from your KTP',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
|
// ID Card Info Form (with pre-filled data)
|
||||||
IdInfoForm(controller: controller, isOfficer: isOfficer),
|
IdInfoForm(controller: controller, isOfficer: isOfficer),
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwSections),
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
|
// Save & Submit Button
|
||||||
|
Obx(
|
||||||
|
() => ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
controller.isSavingData.value
|
||||||
|
? null
|
||||||
|
: () => _submitRegistrationData(controller, context),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: TColors.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
minimumSize: const Size(double.infinity, 50),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||||
|
),
|
||||||
|
disabledBackgroundColor: TColors.primary.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
child:
|
||||||
|
controller.isSavingData.value
|
||||||
|
? const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: TSizes.sm),
|
||||||
|
Text('Submitting...'),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: const Text('Submit Registration'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Save Result Message
|
||||||
|
Obx(
|
||||||
|
() =>
|
||||||
|
controller.dataSaveMessage.value.isNotEmpty
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(top: TSizes.sm),
|
||||||
|
child: Text(
|
||||||
|
controller.dataSaveMessage.value,
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
controller.isDataSaved.value
|
||||||
|
? Colors.green
|
||||||
|
: TColors.error,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build Verification Progress Card
|
||||||
|
Widget _buildVerificationProgressCard(
|
||||||
|
IdentityVerificationController controller,
|
||||||
|
) {
|
||||||
|
final bool allVerified =
|
||||||
|
controller.isBasicInfoVerified.value &&
|
||||||
|
controller.isIdCardVerified.value &&
|
||||||
|
controller.isSelfieVerified.value;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(TSizes.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
allVerified
|
||||||
|
? Colors.green.withOpacity(0.1)
|
||||||
|
: TColors.warning.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
allVerified
|
||||||
|
? Colors.green.withOpacity(0.5)
|
||||||
|
: TColors.warning.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
allVerified ? Icons.verified : Icons.info_outline,
|
||||||
|
color: allVerified ? Colors.green : TColors.warning,
|
||||||
|
size: TSizes.iconMd,
|
||||||
|
),
|
||||||
|
const SizedBox(width: TSizes.sm),
|
||||||
|
Text(
|
||||||
|
allVerified
|
||||||
|
? 'All verification steps completed!'
|
||||||
|
: 'Verification Status',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: allVerified ? Colors.green : TColors.warning,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: TSizes.sm),
|
||||||
|
|
||||||
|
// Basic Info status
|
||||||
|
_buildVerificationItem(
|
||||||
|
'Basic Information',
|
||||||
|
controller.isBasicInfoVerified.value,
|
||||||
|
),
|
||||||
|
|
||||||
|
// ID Card status
|
||||||
|
_buildVerificationItem(
|
||||||
|
'ID Card Verification',
|
||||||
|
controller.isIdCardVerified.value,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Selfie status
|
||||||
|
_buildVerificationItem(
|
||||||
|
'Selfie Verification',
|
||||||
|
controller.isSelfieVerified.value,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Identity Verification (this step)
|
||||||
|
_buildVerificationItem(
|
||||||
|
'Identity Confirmation',
|
||||||
|
controller.isFormValid.value,
|
||||||
|
isCurrentStep: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build verification step item
|
||||||
|
Widget _buildVerificationItem(
|
||||||
|
String title,
|
||||||
|
bool isVerified, {
|
||||||
|
bool isCurrentStep = false,
|
||||||
|
}) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: TSizes.xs),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
isVerified
|
||||||
|
? Icons.check_circle
|
||||||
|
: isCurrentStep
|
||||||
|
? Icons.edit
|
||||||
|
: Icons.error_outline,
|
||||||
|
color:
|
||||||
|
isVerified
|
||||||
|
? Colors.green
|
||||||
|
: isCurrentStep
|
||||||
|
? TColors.primary
|
||||||
|
: TColors.error,
|
||||||
|
size: TSizes.iconSm,
|
||||||
|
),
|
||||||
|
const SizedBox(width: TSizes.xs),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TSizes.fontSizeSm,
|
||||||
|
color:
|
||||||
|
isVerified
|
||||||
|
? Colors.green
|
||||||
|
: isCurrentStep
|
||||||
|
? TColors.primary
|
||||||
|
: TColors.textSecondary,
|
||||||
|
fontWeight: isCurrentStep ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
isVerified
|
||||||
|
? 'Verified'
|
||||||
|
: isCurrentStep
|
||||||
|
? 'In Progress'
|
||||||
|
: 'Not Verified',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TSizes.fontSizeXs,
|
||||||
|
color:
|
||||||
|
isVerified
|
||||||
|
? Colors.green
|
||||||
|
: isCurrentStep
|
||||||
|
? TColors.primary
|
||||||
|
: TColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Submit registration data
|
||||||
|
void _submitRegistrationData(
|
||||||
|
IdentityVerificationController controller,
|
||||||
|
BuildContext context,
|
||||||
|
) async {
|
||||||
|
final formKey = FormRegistrationController().formKey;
|
||||||
|
// Validate form
|
||||||
|
if (!controller.validate(formKey)) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Please complete all required fields'),
|
||||||
|
backgroundColor: TColors.error,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save registration data
|
||||||
|
final result = await controller.saveRegistrationData();
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
// Navigate to success page or show success dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.check_circle, color: Colors.green),
|
||||||
|
SizedBox(width: TSizes.sm),
|
||||||
|
Text('Registration Successful'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
'Your registration has been submitted successfully. You will be notified once your account is verified.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
// Navigate to login or home page
|
||||||
|
Get.offAllNamed('/login');
|
||||||
|
},
|
||||||
|
child: Text('Go to Login'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,152 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
|
||||||
|
class VerificationSummary extends StatelessWidget {
|
||||||
|
final Map<String, dynamic> summaryData;
|
||||||
|
final bool isOfficer;
|
||||||
|
|
||||||
|
const VerificationSummary({
|
||||||
|
super.key,
|
||||||
|
required this.summaryData,
|
||||||
|
required this.isOfficer,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.withOpacity(0.05),
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||||
|
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(TSizes.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: TColors.primary.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(TSizes.borderRadiusMd),
|
||||||
|
topRight: Radius.circular(TSizes.borderRadiusMd),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.summarize,
|
||||||
|
color: TColors.primary,
|
||||||
|
size: TSizes.iconMd,
|
||||||
|
),
|
||||||
|
const SizedBox(width: TSizes.sm),
|
||||||
|
Text(
|
||||||
|
'Registration Summary',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: TColors.primary,
|
||||||
|
fontSize: TSizes.fontSizeMd,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Basic Info Section
|
||||||
|
_buildSectionHeader(context, 'Basic Information'),
|
||||||
|
_buildInfoItem('Email', summaryData['email'] ?? '-'),
|
||||||
|
_buildInfoItem('Phone', summaryData['phone'] ?? '-'),
|
||||||
|
_buildInfoItem('Role', summaryData['role'] ?? '-'),
|
||||||
|
|
||||||
|
// ID Card Section
|
||||||
|
_buildSectionHeader(
|
||||||
|
context,
|
||||||
|
'${isOfficer ? 'KTA' : 'KTP'} Verification',
|
||||||
|
),
|
||||||
|
_buildInfoItem(
|
||||||
|
'${isOfficer ? 'KTA' : 'KTP'} Verified',
|
||||||
|
(summaryData['idCardValid'] ?? false) ? 'Yes' : 'No',
|
||||||
|
),
|
||||||
|
if (!isOfficer) _buildInfoItem('NIK', summaryData['nik'] ?? '-'),
|
||||||
|
_buildInfoItem('Full Name', summaryData['fullName'] ?? '-'),
|
||||||
|
if (!isOfficer)
|
||||||
|
_buildInfoItem(
|
||||||
|
'Place of Birth',
|
||||||
|
summaryData['placeOfBirth'] ?? '-',
|
||||||
|
),
|
||||||
|
_buildInfoItem('Birth Date', summaryData['birthDate'] ?? '-'),
|
||||||
|
_buildInfoItem('Gender', summaryData['gender'] ?? '-'),
|
||||||
|
if (!isOfficer)
|
||||||
|
_buildInfoItem('Address', summaryData['address'] ?? '-'),
|
||||||
|
|
||||||
|
// Selfie Verification Section
|
||||||
|
_buildSectionHeader(context, 'Selfie Verification'),
|
||||||
|
_buildInfoItem(
|
||||||
|
'Liveness Check',
|
||||||
|
(summaryData['livenessCheckPassed'] ?? false)
|
||||||
|
? 'Passed'
|
||||||
|
: 'Not Verified',
|
||||||
|
),
|
||||||
|
_buildInfoItem(
|
||||||
|
'Face Match with ID Card',
|
||||||
|
(summaryData['faceMatchResult'] ?? false)
|
||||||
|
? 'Matched (${((summaryData['faceMatchConfidence'] ?? 0.0) * 100).toStringAsFixed(1)}% confidence)'
|
||||||
|
: 'Not Matched',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionHeader(BuildContext context, String title) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: TSizes.md,
|
||||||
|
vertical: TSizes.sm,
|
||||||
|
),
|
||||||
|
color: Colors.grey.withOpacity(0.1),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoItem(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: TSizes.md,
|
||||||
|
vertical: TSizes.sm,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: TColors.textSecondary,
|
||||||
|
fontSize: TSizes.fontSizeSm,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: TSizes.sm),
|
||||||
|
Expanded(
|
||||||
|
flex: 3,
|
||||||
|
child: Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: TSizes.fontSizeSm,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -340,4 +340,117 @@ class UserRepository extends GetxController {
|
||||||
return false; // Default to not banned
|
return false; // Default to not banned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update user profile with registration data
|
||||||
|
Future<bool> updateUserProfile(Map<String, dynamic> data) async {
|
||||||
|
try {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
throw 'User not authenticated';
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = data['user_id'] ?? currentUserId;
|
||||||
|
if (userId == null) {
|
||||||
|
throw 'User ID is required';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user metadata
|
||||||
|
final authData = {
|
||||||
|
'email': data['email'],
|
||||||
|
'phone': data['phone'],
|
||||||
|
'is_officer': data['is_officer'] ?? false,
|
||||||
|
'role_id': data['role_id'],
|
||||||
|
'profile_status': data['profile_status'] ?? 'pending_approval',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add ID card and verification data to user metadata
|
||||||
|
if (data['id_card'] != null) {
|
||||||
|
authData['id_card'] = data['id_card'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data['face_verification'] != null) {
|
||||||
|
authData['face_verification'] = data['face_verification'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add officer-specific data if user is an officer
|
||||||
|
if (data['is_officer'] == true && data['officer'] != null) {
|
||||||
|
authData['officer_data'] = data['officer'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user metadata in auth
|
||||||
|
await _supabase.auth.updateUser(UserAttributes(data: authData));
|
||||||
|
|
||||||
|
// Update the database tables based on user type
|
||||||
|
if (data['is_officer'] == true) {
|
||||||
|
// Handle officer data
|
||||||
|
if (data['officer'] != null) {
|
||||||
|
final officerData = Map<String, dynamic>.from(data['officer']);
|
||||||
|
|
||||||
|
// Check if officer exists
|
||||||
|
final existingOfficer =
|
||||||
|
await _supabase
|
||||||
|
.from('officers')
|
||||||
|
.select('id')
|
||||||
|
.eq('id', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (existingOfficer != null) {
|
||||||
|
// Update existing officer
|
||||||
|
await _supabase
|
||||||
|
.from('officers')
|
||||||
|
.update(officerData)
|
||||||
|
.eq('id', userId);
|
||||||
|
} else {
|
||||||
|
// Create new officer
|
||||||
|
await _supabase.from('officers').insert(officerData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle regular user data
|
||||||
|
if (data['profile'] != null) {
|
||||||
|
final profileData = Map<String, dynamic>.from(data['profile']);
|
||||||
|
|
||||||
|
// Check if profile exists
|
||||||
|
final existingProfile =
|
||||||
|
await _supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('id')
|
||||||
|
.eq('user_id', userId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (existingProfile != null) {
|
||||||
|
// Update existing profile
|
||||||
|
await _supabase
|
||||||
|
.from('profiles')
|
||||||
|
.update(profileData)
|
||||||
|
.eq('user_id', userId);
|
||||||
|
} else {
|
||||||
|
// Create new profile
|
||||||
|
await _supabase.from('profiles').insert(profileData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update users table with common data
|
||||||
|
await _supabase
|
||||||
|
.from('users')
|
||||||
|
.update({
|
||||||
|
'phone': data['phone'],
|
||||||
|
'roles_id': data['role_id'],
|
||||||
|
'updated_at': DateTime.now().toIso8601String(),
|
||||||
|
})
|
||||||
|
.eq('id', userId);
|
||||||
|
|
||||||
|
_logger.d('User profile updated successfully');
|
||||||
|
return true;
|
||||||
|
} on PostgrestException catch (error) {
|
||||||
|
_logger.e('PostgrestException in updateUserProfile: ${error.message}');
|
||||||
|
return false;
|
||||||
|
} on AuthException catch (e) {
|
||||||
|
_logger.e('AuthException in updateUserProfile: ${e.message}');
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('Exception in updateUserProfile: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue