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/models/user_metadata_model.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/popups/loaders.dart';
|
||||
|
||||
|
@ -83,7 +84,7 @@ class FormRegistrationController extends GetxController {
|
|||
try {
|
||||
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;
|
||||
|
||||
if (session?.user != null) {
|
||||
|
@ -691,8 +692,7 @@ class FormRegistrationController extends GetxController {
|
|||
submitMessage.value = 'Submitting your registration...';
|
||||
|
||||
// Save all registration data using the identity verification controller
|
||||
final identityController = Get.find<IdentityVerificationController>();
|
||||
final result = await identityController.saveRegistrationData();
|
||||
final result = await saveRegistrationData();
|
||||
|
||||
if (result) {
|
||||
isSubmitSuccess.value = true;
|
||||
|
@ -712,4 +712,155 @@ class FormRegistrationController extends GetxController {
|
|||
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:shared_preferences/shared_preferences.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/kta_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/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/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/viewer-information/personal_info_controller.dart';
|
||||
|
||||
class IdentityVerificationController extends GetxController {
|
||||
// Singleton instance
|
||||
|
@ -21,23 +21,22 @@ class IdentityVerificationController extends GetxController {
|
|||
// Dependencies
|
||||
final bool isOfficer;
|
||||
final AzureOCRService _ocrService = AzureOCRService();
|
||||
// Use FacialVerificationService instead of direct EdgeFunction
|
||||
final FacialVerificationService _faceService =
|
||||
FacialVerificationService.instance;
|
||||
|
||||
// Local storage keys (matching those in IdCardVerificationController)
|
||||
// Local storage keys
|
||||
static const String _kOcrResultsKey = 'ocr_results';
|
||||
static const String _kOcrModelKey = 'ocr_model';
|
||||
static const String _kIdCardTypeKey = 'id_card_type';
|
||||
|
||||
// Controllers
|
||||
// Controllers for form fields
|
||||
final TextEditingController nikController = TextEditingController();
|
||||
final TextEditingController fullNameController = TextEditingController();
|
||||
final TextEditingController placeOfBirthController = TextEditingController();
|
||||
final TextEditingController birthDateController = TextEditingController();
|
||||
final TextEditingController addressController = TextEditingController();
|
||||
|
||||
// Error variables
|
||||
// Form validation errors
|
||||
final RxString nikError = RxString('');
|
||||
final RxString fullNameError = RxString('');
|
||||
final RxString placeOfBirthError = RxString('');
|
||||
|
@ -45,45 +44,51 @@ class IdentityVerificationController extends GetxController {
|
|||
final RxString genderError = RxString('');
|
||||
final RxString addressError = RxString('');
|
||||
|
||||
// Verification states
|
||||
// ID verification states
|
||||
final RxBool isVerifying = RxBool(false);
|
||||
final RxBool isVerified = RxBool(false);
|
||||
final RxString verificationMessage = RxString('');
|
||||
|
||||
// Face verification
|
||||
// Face verification states
|
||||
final RxBool isVerifyingFace = RxBool(false);
|
||||
final RxBool isFaceVerified = RxBool(false);
|
||||
final RxString faceVerificationMessage = RxString('');
|
||||
|
||||
// Use FaceComparisonResult for face verification
|
||||
final Rx<FaceComparisonResult?> faceComparisonResult =
|
||||
Rx<FaceComparisonResult?>(null);
|
||||
|
||||
// Gender selection - initialize with a default value
|
||||
// Gender selection dropdown
|
||||
final Rx<String?> selectedGender = Rx<String?>('Male');
|
||||
|
||||
// Form validation
|
||||
// Form validation state
|
||||
final RxBool isFormValid = RxBool(true);
|
||||
|
||||
// Flag to prevent infinite loop
|
||||
bool _isApplyingData = false;
|
||||
|
||||
// NIK field readonly status
|
||||
// UI control states
|
||||
final RxBool isNikReadOnly = RxBool(false);
|
||||
|
||||
// Properties to store extracted ID card data
|
||||
final String? extractedIdCardNumber;
|
||||
final String? extractedName;
|
||||
final RxBool isPreFilledNik = false.obs;
|
||||
|
||||
// Store the loaded OCR data
|
||||
// Storage for extracted data
|
||||
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 isDataSaved = RxBool(false);
|
||||
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({
|
||||
this.extractedIdCardNumber = '',
|
||||
this.extractedName = '',
|
||||
|
@ -93,14 +98,173 @@ class IdentityVerificationController extends GetxController {
|
|||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
// Make sure selectedGender has a default value
|
||||
// Set default gender value
|
||||
selectedGender.value = selectedGender.value ?? 'Male';
|
||||
|
||||
// Load OCR data from local storage with debug info
|
||||
print(
|
||||
'Initializing IdentityVerificationController and loading OCR data...',
|
||||
);
|
||||
loadOcrDataFromLocalStorage();
|
||||
// Load data in sequence
|
||||
_initializeData();
|
||||
}
|
||||
|
||||
// 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
|
||||
|
@ -108,7 +272,6 @@ class IdentityVerificationController extends GetxController {
|
|||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Load stored ID card type to verify it matches current flow
|
||||
final String? storedIdCardType = prefs.getString(_kIdCardTypeKey);
|
||||
print(
|
||||
'Stored ID card type: $storedIdCardType, Current isOfficer: $isOfficer',
|
||||
|
@ -121,7 +284,6 @@ class IdentityVerificationController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
// Load OCR results
|
||||
final String? jsonData = prefs.getString(_kOcrResultsKey);
|
||||
if (jsonData != null) {
|
||||
print('Found OCR data in storage: ${jsonData.length} chars');
|
||||
|
@ -129,11 +291,8 @@ class IdentityVerificationController extends GetxController {
|
|||
ocrData.assignAll(results);
|
||||
print('OCR data loaded: ${results.length} items');
|
||||
|
||||
// Load OCR model
|
||||
final String? modelJson = prefs.getString(_kOcrModelKey);
|
||||
if (modelJson != null) {
|
||||
print('Found OCR model in storage: ${modelJson.length} chars');
|
||||
|
||||
try {
|
||||
if (isOfficer) {
|
||||
final ktaModel = KtaModel.fromJson(jsonDecode(modelJson));
|
||||
|
@ -145,7 +304,6 @@ class IdentityVerificationController extends GetxController {
|
|||
applyKtpDataToForm(ktpModel);
|
||||
}
|
||||
isNikReadOnly.value = true;
|
||||
print('NIK field set to read-only');
|
||||
} catch (e) {
|
||||
print('Error parsing model JSON: $e');
|
||||
}
|
||||
|
@ -156,7 +314,6 @@ class IdentityVerificationController extends GetxController {
|
|||
} catch (e) {
|
||||
print('Error loading OCR data from local storage: $e');
|
||||
} finally {
|
||||
// If data wasn't loaded from local storage, try from FormRegistrationController
|
||||
if (ocrData.isEmpty) {
|
||||
print('Falling back to FormRegistrationController data');
|
||||
_safeApplyIdCardData();
|
||||
|
@ -166,24 +323,14 @@ class IdentityVerificationController extends GetxController {
|
|||
|
||||
// Apply KTP data to form
|
||||
void applyKtpDataToForm(KtpModel ktpModel) {
|
||||
if (ktpModel.nik.isNotEmpty) {
|
||||
nikController.text = ktpModel.nik;
|
||||
}
|
||||
|
||||
if (ktpModel.name.isNotEmpty) {
|
||||
fullNameController.text = ktpModel.name;
|
||||
}
|
||||
|
||||
if (ktpModel.birthPlace.isNotEmpty) {
|
||||
if (ktpModel.nik.isNotEmpty) nikController.text = ktpModel.nik;
|
||||
if (ktpModel.name.isNotEmpty) fullNameController.text = ktpModel.name;
|
||||
if (ktpModel.birthPlace.isNotEmpty)
|
||||
placeOfBirthController.text = ktpModel.birthPlace;
|
||||
}
|
||||
|
||||
if (ktpModel.birthDate.isNotEmpty) {
|
||||
if (ktpModel.birthDate.isNotEmpty)
|
||||
birthDateController.text = ktpModel.birthDate;
|
||||
}
|
||||
|
||||
if (ktpModel.gender.isNotEmpty) {
|
||||
// Convert gender to the format expected by the dropdown
|
||||
String gender = ktpModel.gender.toLowerCase();
|
||||
if (gender.contains('laki') || gender == 'male') {
|
||||
selectedGender.value = 'Male';
|
||||
|
@ -192,55 +339,41 @@ class IdentityVerificationController extends GetxController {
|
|||
}
|
||||
}
|
||||
|
||||
if (ktpModel.address.isNotEmpty) {
|
||||
addressController.text = ktpModel.address;
|
||||
}
|
||||
if (ktpModel.address.isNotEmpty) addressController.text = ktpModel.address;
|
||||
|
||||
// Mark as verified since we have validated KTP data
|
||||
// Mark as verified
|
||||
isVerified.value = true;
|
||||
verificationMessage.value = 'KTP information loaded successfully';
|
||||
}
|
||||
|
||||
// Apply KTA data to form
|
||||
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 &&
|
||||
ktaModel.extraData!.containsKey('tanggal_lahir') &&
|
||||
ktaModel.extraData!['tanggal_lahir'] != null) {
|
||||
birthDateController.text = ktaModel.extraData!['tanggal_lahir'];
|
||||
}
|
||||
|
||||
// Mark as verified
|
||||
isVerified.value = true;
|
||||
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() {
|
||||
if (_isApplyingData) return; // Guard against recursive calls
|
||||
if (_isApplyingData) return;
|
||||
|
||||
try {
|
||||
_isApplyingData = true;
|
||||
|
||||
// Check if FormRegistrationController is ready
|
||||
if (!Get.isRegistered<FormRegistrationController>()) {
|
||||
return;
|
||||
}
|
||||
if (!Get.isRegistered<FormRegistrationController>()) return;
|
||||
|
||||
final formController = Get.find<FormRegistrationController>();
|
||||
if (formController.idCardData.value == null) {
|
||||
return;
|
||||
}
|
||||
if (formController.idCardData.value == null) return;
|
||||
|
||||
final idCardData = formController.idCardData.value;
|
||||
|
||||
if (idCardData != null) {
|
||||
// Fill the form with the extracted data
|
||||
if (!isOfficer && idCardData is KtpModel) {
|
||||
applyKtpDataToForm(idCardData);
|
||||
isNikReadOnly.value = true;
|
||||
|
@ -256,11 +389,13 @@ class IdentityVerificationController extends GetxController {
|
|||
}
|
||||
|
||||
// Validate form inputs
|
||||
bool validate(GlobalKey<FormState> formKey) {
|
||||
bool validate(GlobalKey<FormState>? formKey) {
|
||||
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) {
|
||||
// KTP validation
|
||||
if (nikController.text.isEmpty) {
|
||||
nikError.value = 'NIK is required';
|
||||
isFormValid.value = false;
|
||||
|
@ -278,37 +413,64 @@ class IdentityVerificationController extends GetxController {
|
|||
placeOfBirthError.value = 'Place of birth is required';
|
||||
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) {
|
||||
birthDateError.value = 'Birth date is required';
|
||||
isFormValid.value = false;
|
||||
}
|
||||
|
||||
// if (addressController.text.isEmpty) {
|
||||
// addressError.value = 'Address is required';
|
||||
// isFormValid.value = false;
|
||||
// }
|
||||
if (selectedGender.value == null) {
|
||||
genderError.value = 'Gender is required';
|
||||
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;
|
||||
}
|
||||
|
||||
// Verify ID card information against OCR results
|
||||
// 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 with OCR data
|
||||
void verifyIdCardWithOCR() {
|
||||
try {
|
||||
isVerifying.value = true;
|
||||
|
||||
// Compare form input with OCR results
|
||||
final formController = Get.find<FormRegistrationController>();
|
||||
final idCardData = formController.idCardData.value;
|
||||
|
||||
if (idCardData != null) {
|
||||
if (!isOfficer && idCardData is KtpModel) {
|
||||
// Verify NIK matches
|
||||
bool nikMatches = nikController.text == idCardData.nik;
|
||||
|
||||
// Verify name is similar (accounting for slight differences in formatting)
|
||||
bool nameMatches = _compareNames(
|
||||
fullNameController.text,
|
||||
idCardData.name,
|
||||
|
@ -324,7 +486,6 @@ class IdentityVerificationController extends GetxController {
|
|||
'Information doesn\'t match with KTP. Please check and try again.';
|
||||
}
|
||||
} else if (isOfficer && idCardData is KtaModel) {
|
||||
// For officers, verify that the name matches
|
||||
bool nameMatches = _compareNames(
|
||||
fullNameController.text,
|
||||
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) {
|
||||
// Normalize names for comparison
|
||||
String normalizedName1 = name1.toLowerCase().trim().replaceAll(
|
||||
RegExp(r'\s+'),
|
||||
' ',
|
||||
|
@ -366,19 +526,15 @@ class IdentityVerificationController extends GetxController {
|
|||
' ',
|
||||
);
|
||||
|
||||
// Check exact match
|
||||
if (normalizedName1 == normalizedName2) return true;
|
||||
|
||||
// Check if one name is contained within the other
|
||||
if (normalizedName1.contains(normalizedName2) ||
|
||||
normalizedName2.contains(normalizedName1))
|
||||
return true;
|
||||
|
||||
// Split names into parts and check for partial matches
|
||||
var parts1 = normalizedName1.split(' ');
|
||||
var parts2 = normalizedName2.split(' ');
|
||||
|
||||
// Count matching name parts
|
||||
int matches = 0;
|
||||
for (var part1 in parts1) {
|
||||
for (var part2 in parts2) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
// Face verification function using EdgeFunction instead of AWS directly
|
||||
// Verify face match using FacialVerificationService
|
||||
void verifyFaceMatch() {
|
||||
// Set quick verification status for development
|
||||
if (_faceService.skipFaceVerification) {
|
||||
// Development mode - use dummy data
|
||||
isFaceVerified.value = true;
|
||||
faceVerificationMessage.value =
|
||||
'Face verification skipped (development mode)';
|
||||
|
||||
// Create dummy comparison result
|
||||
final idCardController = Get.find<IdCardVerificationController>();
|
||||
final selfieController = Get.find<SelfieVerificationController>();
|
||||
|
||||
if (idCardController.idCardImage.value != null &&
|
||||
selfieController.selfieImage.value != null) {
|
||||
// Set dummy result
|
||||
faceComparisonResult.value = FaceComparisonResult(
|
||||
sourceFace: FaceModel(
|
||||
imagePath: idCardController.idCardImage.value!.path,
|
||||
|
@ -428,18 +581,14 @@ class IdentityVerificationController extends GetxController {
|
|||
message: 'Face verification passed (development mode)',
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
isVerifyingFace.value = true;
|
||||
|
||||
// Get ID card and selfie images
|
||||
final formController = Get.find<FormRegistrationController>();
|
||||
final idCardController = Get.find<IdCardVerificationController>();
|
||||
final selfieController = Get.find<SelfieVerificationController>();
|
||||
|
||||
// Check if we have both images
|
||||
if (idCardController.idCardImage.value == null ||
|
||||
selfieController.selfieImage.value == null) {
|
||||
isFaceVerified.value = false;
|
||||
|
@ -449,17 +598,13 @@ class IdentityVerificationController extends GetxController {
|
|||
return;
|
||||
}
|
||||
|
||||
// Use FacialVerificationService to compare faces
|
||||
_faceService
|
||||
.compareFaces(
|
||||
idCardController.idCardImage.value!,
|
||||
selfieController.selfieImage.value!,
|
||||
)
|
||||
.then((result) {
|
||||
// Store the comparison result
|
||||
faceComparisonResult.value = result;
|
||||
|
||||
// Update verification status
|
||||
isFaceVerified.value = result.isMatch;
|
||||
faceVerificationMessage.value = result.message;
|
||||
})
|
||||
|
@ -473,7 +618,7 @@ class IdentityVerificationController extends GetxController {
|
|||
});
|
||||
}
|
||||
|
||||
// Clear all error messages
|
||||
// Clear all validation errors
|
||||
void clearErrors() {
|
||||
nikError.value = '';
|
||||
fullNameError.value = '';
|
||||
|
@ -481,21 +626,10 @@ class IdentityVerificationController extends GetxController {
|
|||
birthDateError.value = '';
|
||||
genderError.value = '';
|
||||
addressError.value = '';
|
||||
|
||||
isFormValid.value = true;
|
||||
}
|
||||
|
||||
@override
|
||||
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
|
||||
// Prefill form with extracted data
|
||||
void prefillExtractedData() {
|
||||
if (extractedIdCardNumber != null && extractedIdCardNumber!.isNotEmpty) {
|
||||
nikController.text = extractedIdCardNumber!;
|
||||
|
@ -514,13 +648,38 @@ class IdentityVerificationController extends GetxController {
|
|||
isSavingData.value = true;
|
||||
dataSaveMessage.value = 'Saving your registration data...';
|
||||
|
||||
// Ensure the registration service is available();
|
||||
if (!Get.isRegistered<RegistrationService>()) {
|
||||
await Get.putAsync(() async => RegistrationService());
|
||||
} // Get registration service
|
||||
// Final validation
|
||||
if (!validate(null)) {
|
||||
dataSaveMessage.value = 'Please fix the errors before submitting';
|
||||
return false;
|
||||
}
|
||||
|
||||
final registrationService = Get.find<RegistrationService>();
|
||||
final result = await registrationService.saveRegistrationData();
|
||||
// Update summary with final data
|
||||
_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) {
|
||||
isDataSaved.value = true;
|
||||
|
@ -541,4 +700,15 @@ class IdentityVerificationController extends GetxController {
|
|||
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/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/verification_summary.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';
|
||||
|
||||
class IdentityVerificationStep extends StatelessWidget {
|
||||
|
@ -23,21 +25,305 @@ class IdentityVerificationStep extends StatelessWidget {
|
|||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section Header
|
||||
FormSectionHeader(
|
||||
title: 'Additional Information',
|
||||
subtitle: isOfficer
|
||||
? 'Please provide additional personal details'
|
||||
: 'Please verify your KTP information below. NIK field cannot be edited.',
|
||||
title: 'Review & Verification',
|
||||
subtitle:
|
||||
'Please review and confirm your information before submitting',
|
||||
),
|
||||
|
||||
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),
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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