update user dan user profile
This commit is contained in:
parent
e6e0a0ab07
commit
1a6eefe6e3
|
@ -83,7 +83,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
final bool isFirstTime = storage.read('isFirstTime') ?? true;
|
final bool isFirstTime = storage.read('isFirstTime') ?? true;
|
||||||
final isEmailVerified = session?.user.emailConfirmedAt != null;
|
final isEmailVerified = session?.user.emailConfirmedAt != null;
|
||||||
final isProfileComplete =
|
final isProfileComplete =
|
||||||
session?.user.userMetadata?['profile_status'] == 'complete';
|
session?.user.userMetadata?['profile_status'] == 'completed';
|
||||||
|
|
||||||
// Log the current state for debugging
|
// Log the current state for debugging
|
||||||
Logger().d(
|
Logger().d(
|
||||||
|
@ -679,7 +679,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
// Convert to UserModel
|
// Convert to UserModel
|
||||||
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
|
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
|
||||||
completeData,
|
completeData,
|
||||||
profileStatus: 'complete',
|
profileStatus: 'completed',
|
||||||
);
|
);
|
||||||
|
|
||||||
// First update auth metadata
|
// First update auth metadata
|
||||||
|
|
|
@ -4,6 +4,7 @@ import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/registration_data_model.dart';
|
import 'package:sigap/src/features/auth/data/models/registration_data_model.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/identity-verification/identity_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/identity-verification/identity_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart';
|
||||||
|
@ -14,9 +15,12 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/vie
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/index.dart';
|
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/profile_repository.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/features/personalization/data/repositories/users_repository.dart';
|
||||||
|
import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart';
|
||||||
import 'package:sigap/src/utils/constants/num_int.dart';
|
import 'package:sigap/src/utils/constants/num_int.dart';
|
||||||
|
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
||||||
class FormRegistrationController extends GetxController {
|
class FormRegistrationController extends GetxController {
|
||||||
|
@ -225,6 +229,8 @@ class FormRegistrationController extends GetxController {
|
||||||
void _initializeControllers() {
|
void _initializeControllers() {
|
||||||
final isOfficer = registrationData.value.isOfficer;
|
final isOfficer = registrationData.value.isOfficer;
|
||||||
|
|
||||||
|
Logger().d('Initializing controllers with isOfficer: $isOfficer');
|
||||||
|
|
||||||
// Clear existing controllers first to prevent duplicates
|
// Clear existing controllers first to prevent duplicates
|
||||||
_clearExistingControllers();
|
_clearExistingControllers();
|
||||||
|
|
||||||
|
@ -716,7 +722,7 @@ class FormRegistrationController extends GetxController {
|
||||||
// Fixed missing parenthesis
|
// Fixed missing parenthesis
|
||||||
currentStep.value++;
|
currentStep.value++;
|
||||||
} else {
|
} else {
|
||||||
submitForm();
|
// submitForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -771,38 +777,192 @@ class FormRegistrationController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit the entire form using centralized registration data
|
// Update officer information
|
||||||
Future<bool> submitForm() async {
|
// void updateOfficerRegistrationData() async {
|
||||||
if (!validateCurrentStep()) {
|
// try {
|
||||||
print('Form validation failed for step ${currentStep.value}');
|
// isSubmitting.value = true;
|
||||||
return false;
|
// submitMessage.value = 'Submitting officer registration...';
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStep.value < totalSteps - 1) {
|
// final isConnected = await NetworkManager.instance.isConnected();
|
||||||
nextStep();
|
// if (!isConnected) {
|
||||||
return false;
|
// TLoaders.errorSnackBar(
|
||||||
}
|
// title: 'No Internet Connection',
|
||||||
|
// message: 'Please check your internet connection and try again.',
|
||||||
|
// );
|
||||||
|
// isSubmitting.value = false;
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get user ID and other key information
|
||||||
|
// final userId = AuthenticationRepository.instance.authUser!.id;
|
||||||
|
// final email = AuthenticationRepository.instance.authUser!.email;
|
||||||
|
// final roleId =
|
||||||
|
// AuthenticationRepository.instance.authUser!.userMetadata?['role_id']
|
||||||
|
// as String? ??
|
||||||
|
// '';
|
||||||
|
|
||||||
|
// // Create officer model
|
||||||
|
// final officer = OfficerModel(
|
||||||
|
// id: userId,
|
||||||
|
// unitId: unitInfoController?.unitIdController.text ?? '',
|
||||||
|
// roleId: roleId,
|
||||||
|
// nrp: officerInfoController?.nrpController.text ?? '',
|
||||||
|
// name:
|
||||||
|
// '${personalInfoController.firstNameController.text} ${personalInfoController.lastNameController.text}',
|
||||||
|
// rank: officerInfoController?.rankController.text,
|
||||||
|
// position: unitInfoController?.positionController.text,
|
||||||
|
// phone: personalInfoController.phoneController.text,
|
||||||
|
// email: email,
|
||||||
|
// placeOfBirth: identityController.placeOfBirthController.text,
|
||||||
|
// dateOfBirth: _parseBirthDate(
|
||||||
|
// identityController.birthDateController.text,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Prepare auth metadata update
|
||||||
|
// final authMetadata = {
|
||||||
|
// 'profile_status': 'completed',
|
||||||
|
// 'is_officer': true,
|
||||||
|
// 'name': officer.name,
|
||||||
|
// 'nrp': officer.nrp,
|
||||||
|
// 'unit_id': officer.unitId,
|
||||||
|
// 'unit_name': _getUnitNameFromId(officer.unitId),
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Perform updates in a specific order:
|
||||||
|
|
||||||
|
// // 1. Update auth metadata
|
||||||
|
// try {
|
||||||
|
// await UserRepository.instance.updateUserMetadata(
|
||||||
|
// authMetadata,
|
||||||
|
// );
|
||||||
|
// Logger().d('Officer auth metadata updated successfully');
|
||||||
|
// } catch (e) {
|
||||||
|
// Logger().e('Error updating officer auth metadata: $e');
|
||||||
|
// throw Exception('Failed to update officer auth metadata: $e');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 2. Update or create officer record
|
||||||
|
// try {
|
||||||
|
// await OfficerRepository.instance.updateOrCreateOfficer(
|
||||||
|
// officer.toJson(),
|
||||||
|
// );
|
||||||
|
// Logger().d('Officer data updated successfully');
|
||||||
|
// } catch (e) {
|
||||||
|
// Logger().e('Error updating officer data: $e');
|
||||||
|
// throw Exception('Failed to update officer data: $e');
|
||||||
|
// }
|
||||||
|
|
||||||
|
// isSubmitSuccess.value = true;
|
||||||
|
// submitMessage.value = 'Officer registration completed successfully';
|
||||||
|
// } catch (e) {
|
||||||
|
// Logger().e('Error updating officer data: $e');
|
||||||
|
// isSubmitSuccess.value = false;
|
||||||
|
// submitMessage.value =
|
||||||
|
// 'Failed to complete officer registration: ${e.toString()}';
|
||||||
|
// } finally {
|
||||||
|
// isSubmitting.value = false;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Storing registration data to Supabase
|
||||||
|
void updateUserRegistrationData() async {
|
||||||
try {
|
try {
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
submitMessage.value = 'Submitting your registration...';
|
|
||||||
|
|
||||||
final result = await saveRegistrationData();
|
final isConnected = await NetworkManager.instance.isConnected();
|
||||||
|
if (!isConnected) {
|
||||||
if (result) {
|
TLoaders.errorSnackBar(
|
||||||
isSubmitSuccess.value = true;
|
title: 'No Internet Connection',
|
||||||
submitMessage.value = 'Registration completed successfully!';
|
message: 'Please check your internet connection and try again.',
|
||||||
} else {
|
);
|
||||||
isSubmitSuccess.value = false;
|
isSubmitting.value = false;
|
||||||
submitMessage.value = 'Registration failed. Please try again.';
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
final currentUser = await UserRepository.instance.getUserById(
|
||||||
|
AuthenticationRepository.instance.authUser!.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updateUser = currentUser.copyWith(
|
||||||
|
phone: personalInfoController.phoneController.text,
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final updateProfile = currentUser.profile?.copyWith(
|
||||||
|
nik: identityController.nikController.text,
|
||||||
|
firstName: personalInfoController.firstNameController.text,
|
||||||
|
lastName: personalInfoController.lastNameController.text,
|
||||||
|
address: {
|
||||||
|
'full_address': personalInfoController.addressController.text,
|
||||||
|
},
|
||||||
|
placeOfBirth: identityController.placeOfBirthController.text,
|
||||||
|
birthDate: _parseBirthDate(identityController.birthDateController.text),
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger().d('Created user model for update: ${updateUser.toJson()}');
|
||||||
|
|
||||||
|
// Check if NIK is already registered (but ignore if it's the current user's NIK)
|
||||||
|
final isNikTaken = await UserRepository.instance.isNikExists(
|
||||||
|
updateProfile!.nik!,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNikTaken) {
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Error',
|
||||||
|
message: 'NIK already registered to another user!',
|
||||||
|
);
|
||||||
|
isSubmitting.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user model in database
|
||||||
|
final userRepository = UserRepository.instance;
|
||||||
|
final profileRepository = ProfileRepository.instance;
|
||||||
|
|
||||||
|
final userUpdated = await userRepository.updateUser(updateUser);
|
||||||
|
await profileRepository.updateProfile(updateProfile);
|
||||||
|
|
||||||
|
if (userUpdated == null) {
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to update user profile. Please try again.',
|
||||||
|
);
|
||||||
|
isSubmitting.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Update user metadata with common profile info
|
||||||
|
final metadata =
|
||||||
|
UserMetadataModel(profileStatus: 'completed').toAuthMetadataJson();
|
||||||
|
|
||||||
|
// Update metadata first
|
||||||
|
await userRepository.updateUserMetadata(metadata);
|
||||||
|
|
||||||
|
Logger().d('User metadata updated successfully');
|
||||||
|
|
||||||
|
isSubmitSuccess.value = true;
|
||||||
|
submitMessage.value = 'Registration completed successfully';
|
||||||
|
|
||||||
|
Get.off(
|
||||||
|
() => StateScreen(
|
||||||
|
title: 'Registration Completed',
|
||||||
|
subtitle: 'Your registration has been successfully completed.',
|
||||||
|
icon: Icons.check_circle,
|
||||||
|
showButton: true,
|
||||||
|
primaryButtonTitle: 'Go to Home',
|
||||||
|
onPressed: () => AuthenticationRepository.instance.screenRedirect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error submitting form: $e');
|
Logger().e('Error updating registration data: $e');
|
||||||
isSubmitSuccess.value = false;
|
isSubmitSuccess.value = false;
|
||||||
submitMessage.value = 'Error during registration: $e';
|
submitMessage.value = 'Failed to complete registration: ${e.toString()}';
|
||||||
return false;
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to complete registration: ${e.toString()}',
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.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/registration_data_model.dart';
|
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/facial_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/facial_verification_controller.dart';
|
||||||
|
|
||||||
|
@ -545,60 +544,28 @@ class IdentityVerificationController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save registration data using the centralized model
|
// Save registration data using the centralized model
|
||||||
Future<bool> saveRegistrationData() async {
|
void saveRegistrationData() async {
|
||||||
try {
|
try {
|
||||||
isSavingData.value = true;
|
// Call the appropriate registration update function based on user type
|
||||||
dataSaveMessage.value = 'Saving your registration data...';
|
if (isOfficer) {
|
||||||
|
// For officers, use the officer registration update function
|
||||||
// Final validation
|
// mainController.updateOfficerRegistrationData();
|
||||||
if (!validate(null)) {
|
|
||||||
dataSaveMessage.value = 'Please fix the errors before submitting';
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the registration data with identity verification data
|
// For regular users, call the three required updates
|
||||||
final currentIdentityData = IdentityVerificationData(
|
mainController.updateUserRegistrationData();
|
||||||
nik: nikController.text,
|
|
||||||
fullName: fullNameController.text,
|
|
||||||
placeOfBirth: placeOfBirthController.text,
|
|
||||||
birthDate: birthDateController.text,
|
|
||||||
gender: selectedGender.value ?? '',
|
|
||||||
address: addressController.text,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update the centralized registration data
|
|
||||||
mainController.updateStepData<IdentityVerificationData>(
|
|
||||||
currentIdentityData,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update summary with final form data
|
|
||||||
_updateSummaryWithFormData();
|
|
||||||
|
|
||||||
// Send the data to the main controller for submission
|
|
||||||
final result = await mainController.saveRegistrationData(
|
|
||||||
summaryData: summaryData,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result) {
|
|
||||||
isDataSaved.value = true;
|
|
||||||
dataSaveMessage.value = 'Registration data saved successfully!';
|
|
||||||
} else {
|
|
||||||
isDataSaved.value = false;
|
|
||||||
dataSaveMessage.value =
|
|
||||||
'Failed to save registration data. Please try again.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isDataSaved.value = false;
|
isDataSaved.value = false;
|
||||||
dataSaveMessage.value = 'Error saving registration data: $e';
|
dataSaveMessage.value = 'Error saving registration data: $e';
|
||||||
print('Error saving registration data: $e');
|
Logger().e('Error saving registration data: $e');
|
||||||
return false;
|
|
||||||
} finally {
|
} finally {
|
||||||
isSavingData.value = false;
|
isSavingData.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
// Dispose form controllers
|
// Dispose form controllers
|
||||||
|
|
|
@ -55,7 +55,7 @@ class FaceLivenessController extends GetxController {
|
||||||
final successfulSteps = <String>[].obs;
|
final successfulSteps = <String>[].obs;
|
||||||
|
|
||||||
// Countdown timer state
|
// Countdown timer state
|
||||||
final countdownSeconds = 5.obs;
|
final countdownSeconds = 3.obs;
|
||||||
Timer? _countdownTimer;
|
Timer? _countdownTimer;
|
||||||
|
|
||||||
// Image processing
|
// Image processing
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/identity-verification/identity_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/identity-verification/identity_verification_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/unit_info_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/identity-verification/widgets/id_info_form.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/viewer-information/personal_info_controller.dart';
|
||||||
import 'package:sigap/src/shared/widgets/form/form_section_header.dart';
|
import 'package:sigap/src/shared/widgets/form/form_section_header.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
@ -16,8 +19,9 @@ class IdentityVerificationStep extends StatelessWidget {
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
final mainController = Get.find<FormRegistrationController>();
|
final mainController = Get.find<FormRegistrationController>();
|
||||||
final controller = Get.find<IdentityVerificationController>();
|
final controller = Get.find<IdentityVerificationController>();
|
||||||
|
// final personalInfoController = Get.find<PersonalInfoController>();
|
||||||
Get.find<SelfieVerificationController>();
|
// final idCardController = Get.find<IdCardVerificationController>();
|
||||||
|
// final selfieController = Get.find<SelfieVerificationController>();
|
||||||
|
|
||||||
mainController.formKey = formKey;
|
mainController.formKey = formKey;
|
||||||
|
|
||||||
|
@ -39,40 +43,14 @@ class IdentityVerificationStep extends StatelessWidget {
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
// Verification Progress Card
|
// Verification Progress Card
|
||||||
GetBuilder<IdentityVerificationController>(
|
// GetBuilder<IdentityVerificationController>(
|
||||||
builder: (ctrl) => _buildVerificationProgressCard(ctrl),
|
// builder: (ctrl) => _buildVerificationProgressCard(ctrl),
|
||||||
),
|
// ),
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
// Form section header
|
// Data Summary Section
|
||||||
Container(
|
_buildDataSummaryCards(context, isOfficer),
|
||||||
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
|
|
||||||
GetBuilder<IdentityVerificationController>(
|
GetBuilder<IdentityVerificationController>(
|
||||||
id: 'saveButton',
|
id: 'saveButton',
|
||||||
builder:
|
builder:
|
||||||
|
@ -80,7 +58,7 @@ class IdentityVerificationStep extends StatelessWidget {
|
||||||
onPressed:
|
onPressed:
|
||||||
ctrl.isSavingData.value
|
ctrl.isSavingData.value
|
||||||
? null
|
? null
|
||||||
: () => _submitRegistrationData(ctrl, context),
|
: () => _submitRegistrationData(controller, context),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TColors.primary,
|
backgroundColor: TColors.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
@ -138,6 +116,223 @@ class IdentityVerificationStep extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDataSummaryCards(BuildContext context, bool isOfficer) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// User Profile Information Summary
|
||||||
|
_buildUserProfileSummaryCard(context),
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
|
// ID Card Information Summary
|
||||||
|
_buildIdCardSummaryCard(context, isOfficer),
|
||||||
|
|
||||||
|
// Officer Information Card (if applicable)
|
||||||
|
if (isOfficer) ...[
|
||||||
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
_buildOfficerInfoSummaryCard(context),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
|
// Divider to separate summary from the form
|
||||||
|
Divider(color: Colors.grey.withOpacity(0.3), thickness: 1),
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// User Profile Information Summary Card (based on ProfileModel)
|
||||||
|
Widget _buildUserProfileSummaryCard(BuildContext context) {
|
||||||
|
final personalInfoController = Get.find<PersonalInfoController>();
|
||||||
|
final idCardController = Get.find<IdCardVerificationController>();
|
||||||
|
final selfieController = Get.find<SelfieVerificationController>();
|
||||||
|
|
||||||
|
return _buildSummaryCard(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.person,
|
||||||
|
title: 'User Profile Information',
|
||||||
|
content: [
|
||||||
|
_buildInfoRow(
|
||||||
|
'First Name',
|
||||||
|
personalInfoController.firstNameController.text,
|
||||||
|
),
|
||||||
|
_buildInfoRow(
|
||||||
|
'Last Name',
|
||||||
|
personalInfoController.lastNameController.text,
|
||||||
|
),
|
||||||
|
_buildInfoRow('Phone', personalInfoController.phoneController.text),
|
||||||
|
_buildInfoRow(
|
||||||
|
'Bio',
|
||||||
|
personalInfoController.bioController.text.isEmpty
|
||||||
|
? 'Not provided'
|
||||||
|
: personalInfoController.bioController.text,
|
||||||
|
),
|
||||||
|
_buildInfoRow('Address', personalInfoController.addressController.text),
|
||||||
|
],
|
||||||
|
isVerified: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID Card Information Summary Card (based on KTP/KTA models)
|
||||||
|
Widget _buildIdCardSummaryCard(BuildContext context, bool isOfficer) {
|
||||||
|
final idCardController = Get.find<IdCardVerificationController>();
|
||||||
|
|
||||||
|
final List<Widget> idCardInfo = [];
|
||||||
|
String cardTitle = '';
|
||||||
|
|
||||||
|
if (isOfficer && idCardController.ktaModel.value != null) {
|
||||||
|
cardTitle = 'Police Officer Card (KTA)';
|
||||||
|
final kta = idCardController.ktaModel.value!;
|
||||||
|
idCardInfo.addAll([
|
||||||
|
_buildInfoRow('NRP', kta.formattedNrp),
|
||||||
|
_buildInfoRow('Name', kta.name),
|
||||||
|
_buildInfoRow('Police Unit', kta.policeUnit),
|
||||||
|
if (kta.extraData != null && kta.extraData!['pangkat'] != null)
|
||||||
|
_buildInfoRow('Rank', kta.extraData!['pangkat']),
|
||||||
|
]);
|
||||||
|
} else if (!isOfficer && idCardController.ktpModel.value != null) {
|
||||||
|
cardTitle = 'National ID (KTP)';
|
||||||
|
final ktp = idCardController.ktpModel.value!;
|
||||||
|
idCardInfo.addAll([
|
||||||
|
_buildInfoRow('NIK', ktp.formattedNik),
|
||||||
|
_buildInfoRow('Name', ktp.name),
|
||||||
|
_buildInfoRow('Birth Place', ktp.birthPlace),
|
||||||
|
_buildInfoRow('Birth Date', ktp.birthDate),
|
||||||
|
_buildInfoRow('Gender', ktp.gender),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildSummaryCard(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.badge,
|
||||||
|
title: cardTitle,
|
||||||
|
content: idCardInfo,
|
||||||
|
isVerified: idCardController.isIdCardValid.value,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Officer Information Summary Card (based on OfficerModel)
|
||||||
|
Widget _buildOfficerInfoSummaryCard(BuildContext context) {
|
||||||
|
final officerInfoController = Get.find<OfficerInfoController>();
|
||||||
|
final unitInfoController = Get.find<UnitInfoController>();
|
||||||
|
|
||||||
|
final selectedUnit = unitInfoController.availableUnits.firstWhereOrNull(
|
||||||
|
(unit) => unit.codeUnit == unitInfoController.unitIdController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
return _buildSummaryCard(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.security,
|
||||||
|
title: 'Officer Information',
|
||||||
|
content: [
|
||||||
|
_buildInfoRow('NRP', officerInfoController.nrpController.text),
|
||||||
|
_buildInfoRow('Rank', officerInfoController.rankController.text),
|
||||||
|
_buildInfoRow('Position', unitInfoController.positionController.text),
|
||||||
|
_buildInfoRow(
|
||||||
|
'Unit',
|
||||||
|
selectedUnit?.name ?? unitInfoController.unitIdController.text,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
isVerified: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic summary card builder
|
||||||
|
Widget _buildSummaryCard({
|
||||||
|
required BuildContext context,
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required List<Widget> content,
|
||||||
|
required bool isVerified,
|
||||||
|
}) {
|
||||||
|
return Card(
|
||||||
|
elevation: 1,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||||
|
side: BorderSide(
|
||||||
|
color:
|
||||||
|
isVerified
|
||||||
|
? Colors.green.withOpacity(0.5)
|
||||||
|
: TColors.primary.withOpacity(0.5),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(TSizes.md),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
color: isVerified ? Colors.green : TColors.primary,
|
||||||
|
size: TSizes.iconMd,
|
||||||
|
),
|
||||||
|
const SizedBox(width: TSizes.sm),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isVerified ? Colors.green : TColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Icon(
|
||||||
|
isVerified ? Icons.check_circle : Icons.info_outline,
|
||||||
|
color: isVerified ? Colors.green : TColors.warning,
|
||||||
|
size: TSizes.iconSm,
|
||||||
|
),
|
||||||
|
const SizedBox(width: TSizes.xs),
|
||||||
|
Text(
|
||||||
|
isVerified ? 'Verified' : 'Review',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TSizes.fontSizeXs,
|
||||||
|
color: isVerified ? Colors.green : TColors.warning,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
|
// Content
|
||||||
|
...content,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to build info rows in summary cards
|
||||||
|
Widget _buildInfoRow(String label, String value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: TSizes.xs),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 100,
|
||||||
|
child: Text(
|
||||||
|
'$label:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: TSizes.fontSizeSm,
|
||||||
|
color: TColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(value, style: TextStyle(fontSize: TSizes.fontSizeSm)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Build Verification Progress Card
|
// Build Verification Progress Card
|
||||||
Widget _buildVerificationProgressCard(
|
Widget _buildVerificationProgressCard(
|
||||||
IdentityVerificationController controller,
|
IdentityVerificationController controller,
|
||||||
|
@ -196,7 +391,7 @@ class IdentityVerificationStep extends StatelessWidget {
|
||||||
// ID Card status
|
// ID Card status
|
||||||
_buildVerificationItem(
|
_buildVerificationItem(
|
||||||
'ID Card Verification',
|
'ID Card Verification',
|
||||||
controller.isIdCardVerified.value,
|
controller.extractedIdCardNumber!.isNotEmpty,
|
||||||
),
|
),
|
||||||
|
|
||||||
// Selfie status
|
// Selfie status
|
||||||
|
@ -281,50 +476,10 @@ class IdentityVerificationStep extends StatelessWidget {
|
||||||
IdentityVerificationController controller,
|
IdentityVerificationController controller,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) async {
|
) async {
|
||||||
final formKey = FormRegistrationController().formKey;
|
|
||||||
// Validate form
|
// Call saveRegistrationData with all the collected data
|
||||||
if (!controller.validate(formKey)) {
|
controller.saveRegistrationData();
|
||||||
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) {
|
// The rest of submission logic would be handled in the controller
|
||||||
// // 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'),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-ve
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/captured_selfie_view.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/captured_selfie_view.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/countdown_overlay_widget.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/countdown_overlay_widget.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/debug_panel.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/debug_panel.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/instruction_banner.dart';
|
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/verification_progress_widget.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/widgets/verification_progress_widget.dart';
|
||||||
import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart';
|
import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
@ -54,7 +53,8 @@ class LivenessDetectionPage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.black,
|
||||||
|
extendBodyBehindAppBar: true, // Allow content behind app bar
|
||||||
appBar: _buildAppBar(context, controller, selfieController),
|
appBar: _buildAppBar(context, controller, selfieController),
|
||||||
body: GetBuilder<FaceLivenessController>(
|
body: GetBuilder<FaceLivenessController>(
|
||||||
builder: (_) {
|
builder: (_) {
|
||||||
|
@ -70,7 +70,7 @@ class LivenessDetectionPage extends StatelessWidget {
|
||||||
return StateScreen(
|
return StateScreen(
|
||||||
icon: Icons.camera_alt_outlined,
|
icon: Icons.camera_alt_outlined,
|
||||||
title: 'Camera Error',
|
title: 'Camera Error',
|
||||||
subtitle: 'Unable to access camera. Please try again later .',
|
subtitle: 'Unable to access camera. Please try again later.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,33 +103,30 @@ class LivenessDetectionPage extends StatelessWidget {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.transparent, // Transparent background
|
||||||
title: const Text(
|
iconTheme: IconThemeData(color: Colors.white), // White icons
|
||||||
|
title: Text(
|
||||||
'Face Verification',
|
'Face Verification',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black87,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Add debug button
|
// Debug button
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(Icons.bug_report, color: TColors.warning),
|
icon: Icon(Icons.bug_report, color: Colors.white.withOpacity(0.8)),
|
||||||
onPressed:
|
onPressed:
|
||||||
() =>
|
() =>
|
||||||
showLivenessDebugPanel(context, controller, selfieController),
|
showLivenessDebugPanel(context, controller, selfieController),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.black87),
|
icon: Icon(Icons.arrow_back, color: Colors.white),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
dev.log('Back button pressed', name: 'LIVENESS_DEBUG');
|
dev.log('Back button pressed', name: 'LIVENESS_DEBUG');
|
||||||
|
|
||||||
// Ensure we clean up resources when going back
|
|
||||||
controller.handleCancellation();
|
controller.handleCancellation();
|
||||||
|
|
||||||
// This allows the user to go back to selfie verification step
|
|
||||||
Get.back();
|
Get.back();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -138,37 +135,29 @@ class LivenessDetectionPage extends StatelessWidget {
|
||||||
|
|
||||||
// Camera initializing state UI
|
// Camera initializing state UI
|
||||||
Widget _buildCameraInitializingState(FaceLivenessController controller) {
|
Widget _buildCameraInitializingState(FaceLivenessController controller) {
|
||||||
return Obx(() {
|
return Container(
|
||||||
if (controller.status.value == LivenessStatus.detectingFace) {
|
color: Colors.black,
|
||||||
return Center(
|
child: Center(
|
||||||
child: Text(
|
|
||||||
'Camera initialized successfully!',
|
|
||||||
style: TextStyle(fontSize: 16, color: Colors.green),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
color: TColors.primary,
|
color: TColors.primary,
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
SizedBox(height: 24),
|
||||||
Text(
|
Text(
|
||||||
'Initializing camera...',
|
'Initializing camera...',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: Colors.black87,
|
color: Colors.white70,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main detection view UI with the new layout structure
|
// Main detection view UI with the new layout structure
|
||||||
|
@ -179,34 +168,27 @@ class LivenessDetectionPage extends StatelessWidget {
|
||||||
final screenSize = MediaQuery.of(context).size;
|
final screenSize = MediaQuery.of(context).size;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
children: [
|
children: [
|
||||||
// Main content area with specified layout structure
|
// Full screen camera preview
|
||||||
Column(
|
CameraPreviewWidget(
|
||||||
children: [
|
controller: controller,
|
||||||
// 1. Header with instructions (smaller to give more space to camera)
|
screenWidth: screenSize.width,
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
|
||||||
child: InstructionBanner(controller: controller),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 2. Content area with camera preview (expanded to fill available space)
|
|
||||||
Expanded(
|
|
||||||
flex: 8, // Give most of the space to camera
|
|
||||||
child: CameraPreviewWidget(
|
|
||||||
controller: controller,
|
|
||||||
screenWidth: screenSize.width,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// 3. Bottom verification progress list (small fixed height)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: VerificationProgressWidget(controller: controller),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// Overlay components
|
// Progress indicator at the bottom
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.only(bottom: 20),
|
||||||
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
child: VerificationProgressWidget(controller: controller),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Overlay components including countdown
|
||||||
CountdownOverlayWidget(controller: controller),
|
CountdownOverlayWidget(controller: controller),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import 'package:camera/camera.dart';
|
import 'package:camera/camera.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_tabler_icons/flutter_tabler_icons.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
|
||||||
|
// Enum for bracket positions
|
||||||
|
enum BracketPosition { topLeft, topRight, bottomLeft, bottomRight }
|
||||||
|
|
||||||
class CameraPreviewWidget extends StatelessWidget {
|
class CameraPreviewWidget extends StatelessWidget {
|
||||||
final FaceLivenessController controller;
|
final FaceLivenessController controller;
|
||||||
final double screenWidth;
|
final double screenWidth;
|
||||||
|
@ -18,234 +19,199 @@ class CameraPreviewWidget extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final double screenHeight = MediaQuery.of(context).size.height;
|
final screenHeight = MediaQuery.of(context).size.height;
|
||||||
// Calculate available height for camera preview
|
|
||||||
final double availableHeight =
|
|
||||||
screenHeight * 0.6; // Use 60% of screen height
|
|
||||||
|
|
||||||
// Use the smallest dimension to ensure a square preview
|
|
||||||
final double previewSize =
|
|
||||||
availableHeight < screenWidth
|
|
||||||
? availableHeight
|
|
||||||
: screenWidth * 0.92; // Use 92% of screen width if height is large
|
|
||||||
|
|
||||||
return Obx(() {
|
return Obx(() {
|
||||||
// Check if controller is disposed
|
// Check if controller is disposed
|
||||||
if (controller.status.value == LivenessStatus.dispose) {
|
if (controller.status.value == LivenessStatus.dispose) {
|
||||||
return Container(
|
return _buildCameraPlaceholder(context, "Camera is not available");
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
child: Container(
|
|
||||||
width: previewSize,
|
|
||||||
height: previewSize,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
TablerIcons.camera_off,
|
|
||||||
size: 48,
|
|
||||||
color: Colors.grey.shade600,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Camera is not available',
|
|
||||||
style: TextStyle(color: Colors.grey.shade700),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool isInitialized =
|
final bool isInitialized =
|
||||||
controller.cameraController?.value.isInitialized ?? false;
|
controller.cameraController?.value.isInitialized ?? false;
|
||||||
final bool isActive =
|
|
||||||
true; // Always show camera when controller is initialized
|
|
||||||
final bool isCountdown =
|
final bool isCountdown =
|
||||||
controller.status.value == LivenessStatus.countdown;
|
controller.status.value == LivenessStatus.countdown;
|
||||||
|
|
||||||
return Container(
|
if (!isInitialized) {
|
||||||
width: double.infinity,
|
return _buildCameraPlaceholder(context, "Camera is initializing...");
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
}
|
||||||
child: Stack(
|
|
||||||
alignment: Alignment.center,
|
// Calculate aspect ratio to maintain camera feed proportions
|
||||||
children: [
|
final cameraRatio = controller.cameraController!.value.aspectRatio;
|
||||||
// Camera frame/background
|
|
||||||
Container(
|
// Full screen camera preview with overlays
|
||||||
width: previewSize,
|
return Stack(
|
||||||
height: previewSize,
|
children: [
|
||||||
|
// Camera feed takes the full available width and adjusts height based on aspect ratio
|
||||||
|
Positioned.fill(
|
||||||
|
child: ClipRRect(
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 1.1, // Slight zoom for better framing
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1 / cameraRatio,
|
||||||
|
child: CameraPreview(controller.cameraController!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Dark overlay for better visibility of UI elements
|
||||||
|
Positioned.fill(
|
||||||
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.05),
|
gradient: LinearGradient(
|
||||||
borderRadius: BorderRadius.circular(24),
|
begin: Alignment.topCenter,
|
||||||
border: Border.all(color: Colors.grey.shade300, width: 2),
|
end: Alignment.bottomCenter,
|
||||||
boxShadow: [
|
colors: [
|
||||||
BoxShadow(
|
Colors.black.withOpacity(0.3),
|
||||||
color: Colors.black.withOpacity(0.1),
|
Colors.transparent,
|
||||||
blurRadius: 8,
|
Colors.transparent,
|
||||||
spreadRadius: 0,
|
Colors.black.withOpacity(0.3),
|
||||||
),
|
],
|
||||||
],
|
stops: const [0.0, 0.25, 0.75, 1.0],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Face outline guide with corner brackets
|
||||||
|
Center(child: _buildFaceGuideWithBrackets(screenWidth)),
|
||||||
|
|
||||||
|
// Face indicator icon (shows when no face is detected)
|
||||||
|
if (!controller.isFaceInFrame.value)
|
||||||
|
Center(
|
||||||
|
child: Icon(
|
||||||
|
Icons.face,
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
size: 48,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Full camera feed
|
// Instructions banner at the bottom of the screen (fixed position)
|
||||||
if (isInitialized && isActive)
|
Positioned(
|
||||||
SizedBox(
|
bottom: screenHeight * 0.15, // Position from bottom
|
||||||
width: previewSize,
|
left: 0,
|
||||||
height: previewSize,
|
right: 0,
|
||||||
child: ClipRRect(
|
child: Center(
|
||||||
borderRadius: BorderRadius.circular(22),
|
child: Container(
|
||||||
child: Stack(
|
margin: EdgeInsets.symmetric(horizontal: 20),
|
||||||
fit: StackFit.expand,
|
padding: EdgeInsets.symmetric(vertical: 12, horizontal: 20),
|
||||||
children: [
|
|
||||||
Center(
|
|
||||||
child: SizedBox(
|
|
||||||
width: previewSize,
|
|
||||||
height: previewSize,
|
|
||||||
child: FittedBox(
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
child: SizedBox(
|
|
||||||
width:
|
|
||||||
previewSize /
|
|
||||||
controller
|
|
||||||
.cameraController!
|
|
||||||
.value
|
|
||||||
.aspectRatio,
|
|
||||||
height: previewSize,
|
|
||||||
child: CameraPreview(
|
|
||||||
controller.cameraController!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Overlay for better face visibility
|
|
||||||
Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: RadialGradient(
|
|
||||||
colors: [
|
|
||||||
Colors.transparent,
|
|
||||||
Colors.black.withOpacity(0.2),
|
|
||||||
],
|
|
||||||
stops: const [0.7, 1.0],
|
|
||||||
center: Alignment.center,
|
|
||||||
radius: 0.9,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
// Show placeholder when camera is not active
|
|
||||||
Container(
|
|
||||||
width: previewSize,
|
|
||||||
height: previewSize,
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.05),
|
color: Colors.black.withOpacity(0.5),
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Row(
|
||||||
child: Column(
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
Icon(_getInstructionIcon(), color: Colors.white, size: 22),
|
||||||
Icon(
|
SizedBox(width: 12),
|
||||||
Icons.camera_alt_outlined,
|
Flexible(
|
||||||
size: 48,
|
child: Text(
|
||||||
color: Colors.grey.shade600,
|
_getActionText(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Camera is initializing...',
|
|
||||||
style: TextStyle(color: Colors.grey.shade700),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Scanning animation when not in countdown
|
|
||||||
if (isInitialized && isActive && !isCountdown)
|
|
||||||
Positioned(
|
|
||||||
top: previewSize * 0.2, // Position at 20% from the top
|
|
||||||
child: _buildScanningAnimation(previewSize),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Face guide overlay
|
|
||||||
if (isInitialized && isActive)
|
|
||||||
Center(
|
|
||||||
child: Container(
|
|
||||||
width:
|
|
||||||
previewSize *
|
|
||||||
0.7, // Make face guide 70% of the camera preview
|
|
||||||
height: previewSize * 0.7,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(
|
|
||||||
color: _getFaceGuideColor(),
|
|
||||||
width: 2.5,
|
|
||||||
strokeAlign: BorderSide.strokeAlignOutside,
|
|
||||||
),
|
),
|
||||||
shape: BoxShape.circle,
|
],
|
||||||
),
|
|
||||||
child: Obx(
|
|
||||||
() =>
|
|
||||||
controller.isFaceInFrame.value
|
|
||||||
? Center()
|
|
||||||
: Center(
|
|
||||||
child: Icon(
|
|
||||||
Icons.face,
|
|
||||||
color: Colors.white.withOpacity(0.7),
|
|
||||||
size: 48,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// Instructions overlay
|
),
|
||||||
if (isInitialized && isActive && !isCountdown)
|
],
|
||||||
Positioned(
|
|
||||||
bottom: 20,
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black26,
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
_getActionText(),
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build scanning animation widget
|
// Build face guide with corner brackets
|
||||||
Widget _buildScanningAnimation(double previewSize) {
|
Widget _buildFaceGuideWithBrackets(double screenWidth) {
|
||||||
|
final guideWidth = screenWidth * 0.7;
|
||||||
|
final guideHeight =
|
||||||
|
guideWidth * 1.2; // Make it slightly taller than wide for face
|
||||||
|
final color = _getFaceGuideColor();
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: guideWidth,
|
||||||
|
height: guideHeight,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Invisible container to define the area
|
||||||
|
Container(
|
||||||
|
width: guideWidth,
|
||||||
|
height: guideHeight,
|
||||||
|
decoration: BoxDecoration(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Top-left corner
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
child: _buildCornerBracket(color, BracketPosition.topLeft),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Top-right corner
|
||||||
|
Positioned(
|
||||||
|
top: 0,
|
||||||
|
right: 0,
|
||||||
|
child: _buildCornerBracket(color, BracketPosition.topRight),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom-left corner
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
child: _buildCornerBracket(color, BracketPosition.bottomLeft),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Bottom-right corner
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
right: 0,
|
||||||
|
child: _buildCornerBracket(color, BracketPosition.bottomRight),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a corner bracket
|
||||||
|
Widget _buildCornerBracket(Color color, BracketPosition position) {
|
||||||
|
final bracketThickness = 3.0;
|
||||||
|
final bracketLength = 25.0;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: bracketLength,
|
||||||
|
height: bracketLength,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: CornerBracketPainter(
|
||||||
|
color: color,
|
||||||
|
strokeWidth: bracketThickness,
|
||||||
|
position: position,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placeholder widget when camera isn't available/initialized
|
||||||
|
Widget _buildCameraPlaceholder(BuildContext context, String message) {
|
||||||
return Container(
|
return Container(
|
||||||
width: previewSize * 0.8,
|
color: Colors.black,
|
||||||
height: 3,
|
child: Center(
|
||||||
decoration: BoxDecoration(
|
child: Column(
|
||||||
gradient: LinearGradient(
|
mainAxisSize: MainAxisSize.min,
|
||||||
colors: [
|
children: [
|
||||||
Colors.transparent,
|
Icon(
|
||||||
TColors.primary.withOpacity(0.8),
|
Icons.camera_alt_outlined,
|
||||||
Colors.transparent,
|
size: 48,
|
||||||
|
color: Colors.white.withOpacity(0.7),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
message,
|
||||||
|
style: TextStyle(color: Colors.white.withOpacity(0.7)),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -256,7 +222,7 @@ class CameraPreviewWidget extends StatelessWidget {
|
||||||
String _getActionText() {
|
String _getActionText() {
|
||||||
switch (controller.status.value) {
|
switch (controller.status.value) {
|
||||||
case LivenessStatus.detectingFace:
|
case LivenessStatus.detectingFace:
|
||||||
return 'Position your face within the circle';
|
return 'Position your face within the frame';
|
||||||
case LivenessStatus.checkLeftRotation:
|
case LivenessStatus.checkLeftRotation:
|
||||||
return 'Turn your head to the left';
|
return 'Turn your head to the left';
|
||||||
case LivenessStatus.checkRightRotation:
|
case LivenessStatus.checkRightRotation:
|
||||||
|
@ -267,11 +233,34 @@ class CameraPreviewWidget extends StatelessWidget {
|
||||||
return 'Keep your eyes open';
|
return 'Keep your eyes open';
|
||||||
case LivenessStatus.readyForPhoto:
|
case LivenessStatus.readyForPhoto:
|
||||||
return 'Perfect! Hold still';
|
return 'Perfect! Hold still';
|
||||||
|
case LivenessStatus.countdown:
|
||||||
|
return 'Please hold your pose';
|
||||||
default:
|
default:
|
||||||
return 'Follow instructions';
|
return 'Follow instructions';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get appropriate icon for instructions
|
||||||
|
IconData _getInstructionIcon() {
|
||||||
|
switch (controller.status.value) {
|
||||||
|
case LivenessStatus.detectingFace:
|
||||||
|
return Icons.face;
|
||||||
|
case LivenessStatus.checkLeftRotation:
|
||||||
|
return Icons.rotate_left;
|
||||||
|
case LivenessStatus.checkRightRotation:
|
||||||
|
return Icons.rotate_right;
|
||||||
|
case LivenessStatus.checkSmile:
|
||||||
|
return Icons.sentiment_satisfied_alt;
|
||||||
|
case LivenessStatus.checkEyesOpen:
|
||||||
|
return Icons.remove_red_eye;
|
||||||
|
case LivenessStatus.readyForPhoto:
|
||||||
|
case LivenessStatus.countdown:
|
||||||
|
return Icons.photo_camera;
|
||||||
|
default:
|
||||||
|
return Icons.info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to color the face guide based on detection state
|
// Function to color the face guide based on detection state
|
||||||
Color _getFaceGuideColor() {
|
Color _getFaceGuideColor() {
|
||||||
if (controller.status.value == LivenessStatus.countdown) {
|
if (controller.status.value == LivenessStatus.countdown) {
|
||||||
|
@ -281,7 +270,60 @@ class CameraPreviewWidget extends StatelessWidget {
|
||||||
? Colors.green
|
? Colors.green
|
||||||
: TColors.primary;
|
: TColors.primary;
|
||||||
} else {
|
} else {
|
||||||
return Colors.white.withOpacity(0.7);
|
return Colors.white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom painter for corner brackets
|
||||||
|
class CornerBracketPainter extends CustomPainter {
|
||||||
|
final Color color;
|
||||||
|
final double strokeWidth;
|
||||||
|
final BracketPosition position;
|
||||||
|
|
||||||
|
CornerBracketPainter({
|
||||||
|
required this.color,
|
||||||
|
required this.strokeWidth,
|
||||||
|
required this.position,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final paint =
|
||||||
|
Paint()
|
||||||
|
..color = color
|
||||||
|
..strokeWidth = strokeWidth
|
||||||
|
..strokeCap = StrokeCap.square
|
||||||
|
..style = PaintingStyle.stroke;
|
||||||
|
|
||||||
|
final path = Path();
|
||||||
|
|
||||||
|
switch (position) {
|
||||||
|
case BracketPosition.topLeft:
|
||||||
|
path.moveTo(0, size.height * 0.4);
|
||||||
|
path.lineTo(0, 0);
|
||||||
|
path.lineTo(size.width * 0.4, 0);
|
||||||
|
break;
|
||||||
|
case BracketPosition.topRight:
|
||||||
|
path.moveTo(size.width * 0.6, 0);
|
||||||
|
path.lineTo(size.width, 0);
|
||||||
|
path.lineTo(size.width, size.height * 0.4);
|
||||||
|
break;
|
||||||
|
case BracketPosition.bottomLeft:
|
||||||
|
path.moveTo(0, size.height * 0.6);
|
||||||
|
path.lineTo(0, size.height);
|
||||||
|
path.lineTo(size.width * 0.4, size.height);
|
||||||
|
break;
|
||||||
|
case BracketPosition.bottomRight:
|
||||||
|
path.moveTo(size.width * 0.6, size.height);
|
||||||
|
path.lineTo(size.width, size.height);
|
||||||
|
path.lineTo(size.width, size.height * 0.6);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.drawPath(path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/face_liveness_detection_controller.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
|
||||||
|
|
||||||
class CountdownOverlayWidget extends StatelessWidget {
|
class CountdownOverlayWidget extends StatelessWidget {
|
||||||
final FaceLivenessController controller;
|
final FaceLivenessController controller;
|
||||||
|
@ -9,45 +9,55 @@ class CountdownOverlayWidget extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Directly access the observable values without additional wrapping
|
return Obx(() {
|
||||||
final isCountdown = controller.status.value == LivenessStatus.countdown;
|
final isCountdown = controller.status.value == LivenessStatus.countdown;
|
||||||
final countdownValue = controller.countdownSeconds.value;
|
|
||||||
|
|
||||||
if (!isCountdown) {
|
if (!isCountdown) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return _buildMinimalistCountdown(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMinimalistCountdown(BuildContext context) {
|
||||||
return Positioned.fill(
|
return Positioned.fill(
|
||||||
child: Container(
|
child: Center(
|
||||||
color: Colors.black.withOpacity(0.3),
|
child: Obx(() {
|
||||||
child: Center(
|
final countdownValue = controller.countdownSeconds.value;
|
||||||
child: AnimatedScale(
|
|
||||||
scale: isCountdown ? 1.0 : 0.0,
|
return AnimatedSwitcher(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
child: Container(
|
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||||
width: 120,
|
return ScaleTransition(
|
||||||
height: 120,
|
scale: Tween<double>(begin: 0.8, end: 1.0).animate(
|
||||||
decoration: BoxDecoration(
|
CurvedAnimation(
|
||||||
color: Colors.black.withOpacity(0.7),
|
parent: animation,
|
||||||
shape: BoxShape.circle,
|
curve: Curves.easeOutCubic,
|
||||||
border: Border.all(
|
|
||||||
color: TColors.primary.withOpacity(0.5),
|
|
||||||
width: 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
'$countdownValue',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 60,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: FadeTransition(opacity: animation, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'$countdownValue',
|
||||||
|
key: ValueKey<int>(countdownValue),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w300,
|
||||||
|
fontSize: 120,
|
||||||
|
height: 1.0,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
color: Colors.black.withOpacity(0.3),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,88 +15,105 @@ class VerificationProgressWidget extends StatelessWidget {
|
||||||
final completedSteps = controller.successfulSteps;
|
final completedSteps = controller.successfulSteps;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Progress indicator
|
// Progress indicator
|
||||||
LinearProgressIndicator(
|
Stack(
|
||||||
value: steps.isEmpty ? 0 : completedSteps.length / steps.length,
|
children: [
|
||||||
backgroundColor: Colors.grey.shade200,
|
// Background of the progress bar
|
||||||
color: TColors.primary,
|
Container(
|
||||||
minHeight: 6,
|
width: double.infinity,
|
||||||
borderRadius: BorderRadius.circular(3),
|
height: 6,
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// Completed steps
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: List.generate(steps.length, (index) {
|
|
||||||
final isCompleted = index < controller.currentStepIndex;
|
|
||||||
final isInProgress = index == controller.currentStepIndex;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 10,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color: Colors.white.withOpacity(0.3),
|
||||||
isCompleted
|
borderRadius: BorderRadius.circular(3),
|
||||||
? Colors.green.withOpacity(0.1)
|
),
|
||||||
: isInProgress
|
),
|
||||||
? TColors.primary.withOpacity(0.1)
|
|
||||||
: Colors.grey.withOpacity(0.1),
|
// Filled part of the progress bar
|
||||||
borderRadius: BorderRadius.circular(12),
|
FractionallySizedBox(
|
||||||
border: Border.all(
|
widthFactor:
|
||||||
color:
|
steps.isEmpty ? 0 : completedSteps.length / steps.length,
|
||||||
isCompleted
|
child: Container(
|
||||||
? Colors.green.withOpacity(0.3)
|
height: 6,
|
||||||
: isInProgress
|
decoration: BoxDecoration(
|
||||||
? TColors.primary.withOpacity(0.3)
|
gradient: LinearGradient(
|
||||||
: Colors.grey.withOpacity(0.3),
|
colors: [TColors.primary, Colors.greenAccent],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: TColors.primary.withOpacity(0.5),
|
||||||
|
blurRadius: 6,
|
||||||
|
spreadRadius: 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
),
|
||||||
mainAxisSize: MainAxisSize.min,
|
],
|
||||||
children: [
|
),
|
||||||
Icon(
|
|
||||||
isCompleted
|
const SizedBox(height: 12),
|
||||||
? Icons.check_circle
|
|
||||||
: isInProgress
|
// Completed steps as a row of chips
|
||||||
? Icons.timelapse
|
SingleChildScrollView(
|
||||||
: Icons.circle_outlined,
|
scrollDirection: Axis.horizontal,
|
||||||
size: 16,
|
physics: BouncingScrollPhysics(),
|
||||||
color:
|
child: Row(
|
||||||
isCompleted
|
children: List.generate(steps.length, (index) {
|
||||||
? Colors.green
|
final isCompleted = index < controller.currentStepIndex;
|
||||||
: isInProgress
|
final isInProgress = index == controller.currentStepIndex;
|
||||||
? TColors.primary
|
|
||||||
: Colors.grey,
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
decoration: BoxDecoration(
|
||||||
Text(
|
color: _getStepBackgroundColor(
|
||||||
_getShortStepName(steps[index]),
|
isCompleted,
|
||||||
style: TextStyle(
|
isInProgress,
|
||||||
fontSize: 12,
|
),
|
||||||
color:
|
borderRadius: BorderRadius.circular(16),
|
||||||
isCompleted
|
border: Border.all(
|
||||||
? Colors.green
|
color: _getStepBorderColor(isCompleted, isInProgress),
|
||||||
: isInProgress
|
width: 1.5,
|
||||||
? TColors.primary
|
|
||||||
: Colors.grey,
|
|
||||||
fontWeight:
|
|
||||||
isInProgress
|
|
||||||
? FontWeight.bold
|
|
||||||
: FontWeight.normal,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
child: Row(
|
||||||
),
|
mainAxisSize: MainAxisSize.min,
|
||||||
);
|
children: [
|
||||||
}),
|
Icon(
|
||||||
|
_getStepIcon(isCompleted, isInProgress, index),
|
||||||
|
size: 16,
|
||||||
|
color: _getStepIconColor(isCompleted, isInProgress),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
_getShortStepName(steps[index]),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight:
|
||||||
|
isInProgress
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
color: _getStepTextColor(
|
||||||
|
isCompleted,
|
||||||
|
isInProgress,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -104,6 +121,73 @@ class VerificationProgressWidget extends StatelessWidget {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get step background color based on state
|
||||||
|
Color _getStepBackgroundColor(bool isCompleted, bool isInProgress) {
|
||||||
|
if (isCompleted) {
|
||||||
|
return Colors.green.withOpacity(0.2);
|
||||||
|
} else if (isInProgress) {
|
||||||
|
return TColors.primary.withOpacity(0.2);
|
||||||
|
} else {
|
||||||
|
return Colors.white.withOpacity(0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get step border color based on state
|
||||||
|
Color _getStepBorderColor(bool isCompleted, bool isInProgress) {
|
||||||
|
if (isCompleted) {
|
||||||
|
return Colors.green.withOpacity(0.6);
|
||||||
|
} else if (isInProgress) {
|
||||||
|
return TColors.primary.withOpacity(0.6);
|
||||||
|
} else {
|
||||||
|
return Colors.white.withOpacity(0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get step icon based on state and index
|
||||||
|
IconData _getStepIcon(bool isCompleted, bool isInProgress, int index) {
|
||||||
|
if (isCompleted) {
|
||||||
|
return Icons.check_circle;
|
||||||
|
} else if (isInProgress) {
|
||||||
|
return Icons.timelapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Different icons for different steps
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return Icons.rotate_left;
|
||||||
|
case 1:
|
||||||
|
return Icons.rotate_right;
|
||||||
|
case 2:
|
||||||
|
return Icons.sentiment_satisfied_alt;
|
||||||
|
case 3:
|
||||||
|
return Icons.remove_red_eye;
|
||||||
|
default:
|
||||||
|
return Icons.circle_outlined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get step icon color
|
||||||
|
Color _getStepIconColor(bool isCompleted, bool isInProgress) {
|
||||||
|
if (isCompleted) {
|
||||||
|
return Colors.green;
|
||||||
|
} else if (isInProgress) {
|
||||||
|
return TColors.primary;
|
||||||
|
} else {
|
||||||
|
return Colors.white.withOpacity(0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get step text color
|
||||||
|
Color _getStepTextColor(bool isCompleted, bool isInProgress) {
|
||||||
|
if (isCompleted) {
|
||||||
|
return Colors.green;
|
||||||
|
} else if (isInProgress) {
|
||||||
|
return TColors.primary;
|
||||||
|
} else {
|
||||||
|
return Colors.white.withOpacity(0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Get shorter step names for chips
|
// Get shorter step names for chips
|
||||||
String _getShortStepName(String step) {
|
String _getShortStepName(String step) {
|
||||||
if (step.contains('left')) return 'Look Left';
|
if (step.contains('left')) return 'Look Left';
|
||||||
|
|
|
@ -55,7 +55,7 @@ class OfficerModel {
|
||||||
phone: json['phone'] as String?,
|
phone: json['phone'] as String?,
|
||||||
email: json['email'] as String?,
|
email: json['email'] as String?,
|
||||||
avatar: json['avatar'] as String?,
|
avatar: json['avatar'] as String?,
|
||||||
placeOfBirth: json['place_of_birth'] as String?,
|
placeOfBirth: json['birth_place'] as String?,
|
||||||
dateOfBirth:
|
dateOfBirth:
|
||||||
json['date_of_birth'] != null
|
json['date_of_birth'] != null
|
||||||
? DateTime.parse(json['date_of_birth'] as String)
|
? DateTime.parse(json['date_of_birth'] as String)
|
||||||
|
@ -94,7 +94,7 @@ class OfficerModel {
|
||||||
'phone': phone,
|
'phone': phone,
|
||||||
'email': email,
|
'email': email,
|
||||||
'avatar': avatar,
|
'avatar': avatar,
|
||||||
'place_of_birth': placeOfBirth,
|
'birth_place': placeOfBirth,
|
||||||
'date_of_birth': dateOfBirth?.toIso8601String(),
|
'date_of_birth': dateOfBirth?.toIso8601String(),
|
||||||
'valid_until': validUntil?.toIso8601String(),
|
'valid_until': validUntil?.toIso8601String(),
|
||||||
'qr_code': qrCode,
|
'qr_code': qrCode,
|
||||||
|
@ -165,7 +165,7 @@ class OfficerModel {
|
||||||
phone: officerData['phone'],
|
phone: officerData['phone'],
|
||||||
email: metadata['email'],
|
email: metadata['email'],
|
||||||
avatar: officerData['avatar'],
|
avatar: officerData['avatar'],
|
||||||
placeOfBirth: officerData['place_of_birth'],
|
placeOfBirth: officerData['birth_place'],
|
||||||
dateOfBirth:
|
dateOfBirth:
|
||||||
officerData['date_of_birth'] != null
|
officerData['date_of_birth'] != null
|
||||||
? DateTime.parse(officerData['date_of_birth'])
|
? DateTime.parse(officerData['date_of_birth'])
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
class ProfileModel {
|
class ProfileModel {
|
||||||
final String id;
|
final String id;
|
||||||
final String userId;
|
final String userId;
|
||||||
final String nik;
|
final String? nik;
|
||||||
final String? avatar;
|
final String? avatar;
|
||||||
final String? username;
|
final String? username;
|
||||||
final String? firstName;
|
final String? firstName;
|
||||||
|
@ -31,7 +31,7 @@ class ProfileModel {
|
||||||
return ProfileModel(
|
return ProfileModel(
|
||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
userId: json['user_id'] as String,
|
userId: json['user_id'] as String,
|
||||||
nik: json['nik'] as String,
|
nik: json['nik'] as String?,
|
||||||
avatar: json['avatar'] as String?,
|
avatar: json['avatar'] as String?,
|
||||||
username: json['username'] as String?,
|
username: json['username'] as String?,
|
||||||
firstName: json['first_name'] as String?,
|
firstName: json['first_name'] as String?,
|
||||||
|
@ -41,7 +41,7 @@ class ProfileModel {
|
||||||
json['address'] != null
|
json['address'] != null
|
||||||
? Map<String, dynamic>.from(json['address'] as Map)
|
? Map<String, dynamic>.from(json['address'] as Map)
|
||||||
: null,
|
: null,
|
||||||
placeOfBirth: json['place_of_birth'] as String?,
|
placeOfBirth: json['birth_place'] as String?,
|
||||||
birthDate:
|
birthDate:
|
||||||
json['birth_date'] != null
|
json['birth_date'] != null
|
||||||
? DateTime.parse(json['birth_date'] as String)
|
? DateTime.parse(json['birth_date'] as String)
|
||||||
|
@ -61,7 +61,7 @@ class ProfileModel {
|
||||||
'last_name': lastName,
|
'last_name': lastName,
|
||||||
'bio': bio,
|
'bio': bio,
|
||||||
'address': address,
|
'address': address,
|
||||||
'place_of_birth': placeOfBirth,
|
'birth_place': placeOfBirth,
|
||||||
'birth_date': birthDate?.toIso8601String(),
|
'birth_date': birthDate?.toIso8601String(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,7 +142,7 @@ class UserMetadataModel {
|
||||||
/// Convert to specialized JSON for profile completion
|
/// Convert to specialized JSON for profile completion
|
||||||
Map<String, dynamic> toProfileCompletionJson() {
|
Map<String, dynamic> toProfileCompletionJson() {
|
||||||
return {
|
return {
|
||||||
'profile_status': 'complete',
|
'profile_status': 'completed',
|
||||||
'is_officer': isOfficer,
|
'is_officer': isOfficer,
|
||||||
if (name != null) 'name': name,
|
if (name != null) 'name': name,
|
||||||
if (phone != null) 'phone': phone,
|
if (phone != null) 'phone': phone,
|
||||||
|
|
|
@ -109,8 +109,8 @@ class UserModel {
|
||||||
'updated_at': updatedAt.toIso8601String(),
|
'updated_at': updatedAt.toIso8601String(),
|
||||||
'banned_until': bannedUntil?.toIso8601String(),
|
'banned_until': bannedUntil?.toIso8601String(),
|
||||||
'is_anonymous': isAnonymous,
|
'is_anonymous': isAnonymous,
|
||||||
if (profile != null) 'profiles': profile!.toJson(),
|
// if (profile != null) 'profiles': profile!.toJson(),
|
||||||
if (role != null) 'role': role!.toJson(),
|
// if (role != null) 'role': role!.toJson(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,9 +56,7 @@ class ProfileRepository extends GetxController {
|
||||||
|
|
||||||
final fileName =
|
final fileName =
|
||||||
'${currentUserId}_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
'${currentUserId}_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||||||
await _supabase.storage
|
await _supabase.storage.from('avatars').upload(fileName, File(filePath));
|
||||||
.from('avatars')
|
|
||||||
.upload(fileName, File(filePath));
|
|
||||||
|
|
||||||
final avatarUrl = _supabase.storage
|
final avatarUrl = _supabase.storage
|
||||||
.from('avatars')
|
.from('avatars')
|
||||||
|
@ -84,42 +82,23 @@ class ProfileRepository extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update profile
|
// Update profile
|
||||||
Future<ProfileModel> updateProfile({
|
Future<ProfileModel> updateProfile(ProfileModel profile) async {
|
||||||
String? firstName,
|
|
||||||
String? lastName,
|
|
||||||
String? bio,
|
|
||||||
Map<String, dynamic>? address,
|
|
||||||
DateTime? birthDate,
|
|
||||||
String? avatar,
|
|
||||||
String? username,
|
|
||||||
}) async {
|
|
||||||
try {
|
try {
|
||||||
if (currentUserId == null) {
|
if (currentUserId == null) {
|
||||||
throw 'User not authenticated';
|
throw 'User not authenticated';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build update data object
|
final profileData = profile.toJson();
|
||||||
final Map<String, dynamic> updateData = {};
|
|
||||||
|
|
||||||
if (firstName != null) updateData['first_name'] = firstName;
|
final updatedProfile =
|
||||||
if (lastName != null) updateData['last_name'] = lastName;
|
await _supabase
|
||||||
if (bio != null) updateData['bio'] = bio;
|
.from('profiles')
|
||||||
if (address != null) updateData['address'] = address;
|
.update(profileData)
|
||||||
if (birthDate != null)
|
.eq('user_id', currentUserId!)
|
||||||
updateData['birth_date'] = birthDate.toIso8601String();
|
.select()
|
||||||
if (avatar != null) updateData['avatar'] = avatar;
|
.single();
|
||||||
if (username != null) updateData['username'] = username;
|
|
||||||
|
|
||||||
// Only update if there's data to update
|
return ProfileModel.fromJson(updatedProfile);
|
||||||
if (updateData.isNotEmpty) {
|
|
||||||
await _supabase
|
|
||||||
.from('profiles')
|
|
||||||
.update(updateData)
|
|
||||||
.eq('user_id', currentUserId!);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch and return updated profile
|
|
||||||
return await getProfileData();
|
|
||||||
} on PostgrestException catch (error) {
|
} on PostgrestException catch (error) {
|
||||||
_logger.e('PostgrestException in updateProfile: ${error.message}');
|
_logger.e('PostgrestException in updateProfile: ${error.message}');
|
||||||
throw TExceptions.fromCode(error.code ?? 'unknown-error');
|
throw TExceptions.fromCode(error.code ?? 'unknown-error');
|
||||||
|
@ -194,7 +173,7 @@ class ProfileRepository extends GetxController {
|
||||||
throw 'Failed to fetch profile data: ${e.toString()}';
|
throw 'Failed to fetch profile data: ${e.toString()}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get profile by NIK
|
// Get profile by NIK
|
||||||
Future<ProfileModel?> getProfileByNIK(String nik) async {
|
Future<ProfileModel?> getProfileByNIK(String nik) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -13,12 +13,12 @@ class UserRepository extends GetxController {
|
||||||
static UserRepository get instance => Get.find();
|
static UserRepository get instance => Get.find();
|
||||||
|
|
||||||
final _supabase = SupabaseService.instance.client;
|
final _supabase = SupabaseService.instance.client;
|
||||||
final _logger = Get.find<Logger>();
|
final _logger = Get.put(Logger());
|
||||||
|
|
||||||
// Get current user ID
|
// Get current user ID
|
||||||
String? get currentUserId => SupabaseService.instance.currentUserId;
|
String? get currentUserId => SupabaseService.instance.currentUserId;
|
||||||
|
|
||||||
// Check if user is authenticated
|
// Check if user is aPuthenticated
|
||||||
bool get isAuthenticated => currentUserId != null;
|
bool get isAuthenticated => currentUserId != null;
|
||||||
|
|
||||||
// Check if user is an officer
|
// Check if user is an officer
|
||||||
|
@ -42,7 +42,7 @@ class UserRepository extends GetxController {
|
||||||
.select('role:roles(name)')
|
.select('role:roles(name)')
|
||||||
.eq('id', currentUserId!)
|
.eq('id', currentUserId!)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
final roleName =
|
final roleName =
|
||||||
(userData['role'] as Map<String, dynamic>)['name'] as String;
|
(userData['role'] as Map<String, dynamic>)['name'] as String;
|
||||||
return roleName.toLowerCase() == 'officer';
|
return roleName.toLowerCase() == 'officer';
|
||||||
|
@ -83,6 +83,32 @@ class UserRepository extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<UserModel?> updateUser(UserModel user) async {
|
||||||
|
try {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
throw 'User not authenticated';
|
||||||
|
}
|
||||||
|
|
||||||
|
final userData = user.toJson();
|
||||||
|
|
||||||
|
// Update user data in database
|
||||||
|
await _supabase.from('users').update(userData).eq('id', user.id);
|
||||||
|
|
||||||
|
// Return updated user model
|
||||||
|
return UserModel.fromJson(userData);
|
||||||
|
} on PostgrestException catch (error) {
|
||||||
|
_logger.e('PostgrestException in updateUser: ${error.message}');
|
||||||
|
throw TExceptions.fromCode(error.code ?? 'unknown-error');
|
||||||
|
} on FormatException catch (_) {
|
||||||
|
throw const TFormatException();
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
throw TPlatformException(e.code).message;
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('Exception in updateUser: $e');
|
||||||
|
throw 'Failed to update user data: ${e.toString()}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update user metadata
|
// Update user metadata
|
||||||
Future<void> updateUserMetadata(Map<String, dynamic> metadata) async {
|
Future<void> updateUserMetadata(Map<String, dynamic> metadata) async {
|
||||||
try {
|
try {
|
||||||
|
@ -91,7 +117,6 @@ class UserRepository extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
await _supabase.auth.updateUser(UserAttributes(data: metadata));
|
await _supabase.auth.updateUser(UserAttributes(data: metadata));
|
||||||
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
_logger.e('AuthException in updateUserMetadata: ${e.message}');
|
_logger.e('AuthException in updateUserMetadata: ${e.message}');
|
||||||
throw TExceptions(e.message);
|
throw TExceptions(e.message);
|
||||||
|
@ -141,7 +166,6 @@ class UserRepository extends GetxController {
|
||||||
.from('users')
|
.from('users')
|
||||||
.update({'phone': newPhone})
|
.update({'phone': newPhone})
|
||||||
.eq('id', currentUserId!);
|
.eq('id', currentUserId!);
|
||||||
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
_logger.e('AuthException in updateUserPhone: ${e.message}');
|
_logger.e('AuthException in updateUserPhone: ${e.message}');
|
||||||
throw TExceptions(e.message);
|
throw TExceptions(e.message);
|
||||||
|
@ -162,7 +186,6 @@ class UserRepository extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
await _supabase.auth.updateUser(UserAttributes(password: newPassword));
|
await _supabase.auth.updateUser(UserAttributes(password: newPassword));
|
||||||
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
_logger.e('AuthException in updateUserPassword: ${e.message}');
|
_logger.e('AuthException in updateUserPassword: ${e.message}');
|
||||||
throw TExceptions(e.message);
|
throw TExceptions(e.message);
|
||||||
|
@ -263,6 +286,40 @@ class UserRepository extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get user by nik
|
||||||
|
Future<UserModel?> getUserByNik(String nik) async {
|
||||||
|
try {
|
||||||
|
final userData =
|
||||||
|
await _supabase
|
||||||
|
.from('users')
|
||||||
|
.select('*, profiles(*), role:roles(*)')
|
||||||
|
.eq('profiles.nik', nik)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (userData == null) return null;
|
||||||
|
|
||||||
|
return UserModel.fromJson(userData);
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('Exception in getUserByNik: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isNikExists(String nik) async {
|
||||||
|
try {
|
||||||
|
final result = await _supabase
|
||||||
|
.from('profiles')
|
||||||
|
.select('id')
|
||||||
|
.eq('nik', nik)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return result.isNotEmpty;
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('Exception in isNikExists: $e');
|
||||||
|
return false; // Assume NIK doesn't exist if error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Chekch if email is already in use
|
// Chekch if email is already in use
|
||||||
Future<bool> isEmailInUse(String email) async {
|
Future<bool> isEmailInUse(String email) async {
|
||||||
try {
|
try {
|
||||||
|
@ -283,7 +340,6 @@ class UserRepository extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Search users by name/username/email
|
// Search users by name/username/email
|
||||||
Future<List<UserModel>> searchUsers(String query, {int limit = 20}) async {
|
Future<List<UserModel>> searchUsers(String query, {int limit = 20}) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -37,75 +37,82 @@ class StateScreen extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SingleChildScrollView(
|
body: Center(
|
||||||
child: Padding(
|
child: SingleChildScrollView(
|
||||||
padding: DSpacingStyle.paddingWithAppBarHeight * 2,
|
child: Padding(
|
||||||
child: Center(
|
padding: DSpacingStyle.paddingWithAppBarHeight,
|
||||||
child: Column(
|
child: Container(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
width: double.infinity,
|
||||||
children: [
|
constraints: BoxConstraints(
|
||||||
// Image, Icon, or Lottie
|
maxWidth: THelperFunctions.screenWidth() * 0.9,
|
||||||
if (icon != null)
|
),
|
||||||
Icon(
|
child: Column(
|
||||||
icon,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
size: THelperFunctions.screenWidth() * 0.3,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
color: TColors.primary,
|
children: [
|
||||||
)
|
// Image, Icon, or Lottie
|
||||||
else if (isLottie == true && image != null)
|
if (icon != null)
|
||||||
Lottie.asset(
|
Icon(
|
||||||
image!,
|
icon,
|
||||||
width: THelperFunctions.screenWidth() * 1.5,
|
size: THelperFunctions.screenWidth() * 0.3,
|
||||||
)
|
color: TColors.primary,
|
||||||
else if (image != null)
|
)
|
||||||
Image(
|
else if (isLottie == true && image != null)
|
||||||
image: AssetImage(image!),
|
Lottie.asset(
|
||||||
width: THelperFunctions.screenWidth(),
|
image!,
|
||||||
),
|
width: THelperFunctions.screenWidth() * 0.8,
|
||||||
const SizedBox(height: TSizes.spaceBtwSections),
|
)
|
||||||
|
else if (image != null)
|
||||||
// Title & subtitle
|
Image(
|
||||||
Text(
|
image: AssetImage(image!),
|
||||||
title,
|
width: THelperFunctions.screenWidth() * 0.8,
|
||||||
style: Theme.of(context).textTheme.headlineMedium,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
|
||||||
Text(
|
|
||||||
subtitle,
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: TSizes.spaceBtwSections),
|
|
||||||
|
|
||||||
// Button
|
|
||||||
if (showButton)
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: onPressed,
|
|
||||||
child: Text(primaryButtonTitle),
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
|
||||||
|
|
||||||
if (secondaryButton)
|
// Title & subtitle
|
||||||
SizedBox(
|
Text(
|
||||||
width: double.infinity,
|
title,
|
||||||
child: OutlinedButton(
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
onPressed: onSecondaryPressed,
|
textAlign: TextAlign.center,
|
||||||
style: ElevatedButton.styleFrom(
|
),
|
||||||
side: const BorderSide(color: Colors.transparent),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
),
|
Text(
|
||||||
child: Text(
|
subtitle,
|
||||||
secondaryTitle,
|
style: Theme.of(context).textTheme.labelMedium,
|
||||||
style: const TextStyle(color: TColors.primary),
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
|
// Button
|
||||||
|
if (showButton)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
child: Text(primaryButtonTitle),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
// Additional child widget
|
if (secondaryButton)
|
||||||
if (child != null) child!,
|
SizedBox(
|
||||||
],
|
width: double.infinity,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: onSecondaryPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: Colors.transparent),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
secondaryTitle,
|
||||||
|
style: const TextStyle(color: TColors.primary),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Additional child widget
|
||||||
|
if (child != null) child!,
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in New Issue