feat: Enhance selfie verification step with navigation buttons and validation
- Added StepNavigationButtons to SelfieVerificationStep for improved navigation. - Implemented validation logic to ensure selfie verification is completed before proceeding. - Updated AuthButton to use theme colors for better consistency. - Modified OfficerModel to allow nullable fields and added new fields for enhanced data handling. - Set default role to Viewer in RoleSelectionController. - Updated role selection screen with new asset for improved UI. - Enhanced UserMetadataModel with new methods for updating officer and viewer data. - Added logging to OfficerRepository and UserRepository for better debugging. - Improved image uploader error overlay with a blurred background for better visibility. - Added support for SVG images in StateScreen widget. - Introduced custom input decoration in CustomTextField for more flexibility. - Reduced total steps for officers from 4 to 3 in TNum constants. - Fixed schema for patrol_units in Prisma to ensure correct field definitions. - Created StepNavigationButtons widget for reusable navigation controls across steps.
This commit is contained in:
parent
407233916b
commit
569e1d6049
|
@ -942,12 +942,12 @@ class AzureOCRService {
|
||||||
// First check for "Jabatan:" pattern explicitly
|
// First check for "Jabatan:" pattern explicitly
|
||||||
for (int i = 0; i < allLines.length; i++) {
|
for (int i = 0; i < allLines.length; i++) {
|
||||||
String line = allLines[i].toLowerCase();
|
String line = allLines[i].toLowerCase();
|
||||||
if (line.contains('jabatan')) {
|
if (line.contains('pangkat')) {
|
||||||
if (line.contains(':')) {
|
if (line.contains(':')) {
|
||||||
String position = line.split(':')[1].trim();
|
String position = line.split(':')[1].trim();
|
||||||
extractedInfo['jabatan'] = _normalizeCase(position);
|
extractedInfo['pangkat'] = _normalizeCase(position);
|
||||||
print(
|
print(
|
||||||
'Found position from "jabatan:" pattern: ${extractedInfo['jabatan']}',
|
'Found position from "jabatan:" pattern: ${extractedInfo['pangkat']}',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -955,9 +955,9 @@ class AzureOCRService {
|
||||||
// Check next line
|
// Check next line
|
||||||
if (i + 1 < allLines.length) {
|
if (i + 1 < allLines.length) {
|
||||||
String nextLine = allLines[i + 1].trim();
|
String nextLine = allLines[i + 1].trim();
|
||||||
extractedInfo['jabatan'] = _normalizeCase(nextLine);
|
extractedInfo['pangkat'] = _normalizeCase(nextLine);
|
||||||
print(
|
print(
|
||||||
'Found position from line after "jabatan": ${extractedInfo['jabatan']}',
|
'Found position from line after "jabatan": ${extractedInfo['pangkat']}',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -988,8 +988,8 @@ class AzureOCRService {
|
||||||
// Check if the entire line is a position
|
// Check if the entire line is a position
|
||||||
for (String position in commonPositions) {
|
for (String position in commonPositions) {
|
||||||
if (line == position) {
|
if (line == position) {
|
||||||
extractedInfo['jabatan'] = position;
|
extractedInfo['pangkat'] = position;
|
||||||
print('Found exact position match: ${extractedInfo['jabatan']}');
|
print('Found exact position match: ${extractedInfo['pangkat']}');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -998,8 +998,8 @@ class AzureOCRService {
|
||||||
for (String position in commonPositions) {
|
for (String position in commonPositions) {
|
||||||
if (line.contains(position)) {
|
if (line.contains(position)) {
|
||||||
// Extract just the position part (this is more complex for real cards)
|
// Extract just the position part (this is more complex for real cards)
|
||||||
extractedInfo['jabatan'] = position;
|
extractedInfo['pangkat'] = position;
|
||||||
print('Found position in line: ${extractedInfo['jabatan']}');
|
print('Found position in line: ${extractedInfo['pangkat']}');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1008,7 +1008,7 @@ class AzureOCRService {
|
||||||
// Special handling for the sample data provided
|
// Special handling for the sample data provided
|
||||||
if (allLines.length >= 4 &&
|
if (allLines.length >= 4 &&
|
||||||
allLines[3].trim().toUpperCase() == 'BRIGADIR') {
|
allLines[3].trim().toUpperCase() == 'BRIGADIR') {
|
||||||
extractedInfo['jabatan'] = 'BRIGADIR';
|
extractedInfo['pangkat'] = 'BRIGADIR';
|
||||||
print('Found position (BRIGADIR) at line 3');
|
print('Found position (BRIGADIR) at line 3');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1018,8 +1018,8 @@ class AzureOCRService {
|
||||||
String line = allLines[i].trim().toUpperCase();
|
String line = allLines[i].trim().toUpperCase();
|
||||||
for (String position in commonPositions) {
|
for (String position in commonPositions) {
|
||||||
if (line == position || line.contains(position)) {
|
if (line == position || line.contains(position)) {
|
||||||
extractedInfo['jabatan'] = position;
|
extractedInfo['pangkat'] = position;
|
||||||
print('Found position in full scan: ${extractedInfo['jabatan']}');
|
print('Found position in full scan: ${extractedInfo['pangkat']}');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ class SignInController extends GetxController {
|
||||||
|
|
||||||
// Navigate to sign up screen
|
// Navigate to sign up screen
|
||||||
void goToSignUp() {
|
void goToSignUp() {
|
||||||
Get.toNamed(AppRoutes.signupWithRole);
|
Get.toNamed(AppRoutes.roleSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear error messages
|
// Clear error messages
|
||||||
|
|
|
@ -41,15 +41,23 @@ class FormRegistrationController extends GetxController {
|
||||||
late final OfficerInfoController? officerInfoController;
|
late final OfficerInfoController? officerInfoController;
|
||||||
late final UnitInfoController? unitInfoController;
|
late final UnitInfoController? unitInfoController;
|
||||||
|
|
||||||
late GlobalKey<FormState> formKey;
|
// Current step in the registration process
|
||||||
|
final RxInt currentStep = 0.obs;
|
||||||
|
|
||||||
|
// Total steps based on role
|
||||||
|
int get totalSteps =>
|
||||||
|
selectedRole.value?.isOfficer ?? false
|
||||||
|
? TNum
|
||||||
|
.totalStepOfficer // 3 steps for officers
|
||||||
|
: TNum.totalStepViewer; // 4 steps for viewers
|
||||||
|
|
||||||
final storage = GetStorage();
|
final storage = GetStorage();
|
||||||
|
|
||||||
// Current step index
|
// Loading state for form operations
|
||||||
final RxInt currentStep = 0.obs;
|
final RxBool isLoading = false.obs;
|
||||||
|
|
||||||
// Total number of steps (depends on role)
|
// Form key for validation
|
||||||
late final int totalSteps;
|
GlobalKey<FormState>? formKey;
|
||||||
|
|
||||||
// User metadata model (kept for backward compatibility)
|
// User metadata model (kept for backward compatibility)
|
||||||
final Rx<UserMetadataModel> userMetadata = UserMetadataModel().obs;
|
final Rx<UserMetadataModel> userMetadata = UserMetadataModel().obs;
|
||||||
|
@ -61,9 +69,6 @@ class FormRegistrationController extends GetxController {
|
||||||
// Officer data (kept for backward compatibility)
|
// Officer data (kept for backward compatibility)
|
||||||
final Rx<OfficerModel?> officerModel = Rx<OfficerModel?>(null);
|
final Rx<OfficerModel?> officerModel = Rx<OfficerModel?>(null);
|
||||||
|
|
||||||
// Loading state
|
|
||||||
final RxBool isLoading = false.obs;
|
|
||||||
|
|
||||||
// Form submission states
|
// Form submission states
|
||||||
final RxBool isSubmitting = RxBool(false);
|
final RxBool isSubmitting = RxBool(false);
|
||||||
final RxString submitMessage = RxString('');
|
final RxString submitMessage = RxString('');
|
||||||
|
@ -308,11 +313,9 @@ class FormRegistrationController extends GetxController {
|
||||||
if (isOfficer) {
|
if (isOfficer) {
|
||||||
officerInfoController = Get.find<OfficerInfoController>();
|
officerInfoController = Get.find<OfficerInfoController>();
|
||||||
unitInfoController = Get.find<UnitInfoController>();
|
unitInfoController = Get.find<UnitInfoController>();
|
||||||
totalSteps = TNum.totalStepOfficer;
|
|
||||||
} else {
|
} else {
|
||||||
officerInfoController = null;
|
officerInfoController = null;
|
||||||
unitInfoController = null;
|
unitInfoController = null;
|
||||||
totalSteps = TNum.totalStepViewer;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -562,196 +565,59 @@ class FormRegistrationController extends GetxController {
|
||||||
// Get step titles based on role
|
// Get step titles based on role
|
||||||
List<String> getStepTitles() {
|
List<String> getStepTitles() {
|
||||||
if (selectedRole.value?.isOfficer ?? false) {
|
if (selectedRole.value?.isOfficer ?? false) {
|
||||||
return ['Personal', 'ID Card', 'Selfie', 'Officer Info', 'Unit Info'];
|
// Officer steps - no personal info
|
||||||
|
return ['ID Card', 'Selfie', 'Officer Info'];
|
||||||
} else {
|
} else {
|
||||||
return ['Personal', 'ID Card', 'Selfie', 'Identity'];
|
// Viewer steps - includes personal info
|
||||||
|
return ['Personal Info', 'ID Card', 'Selfie', 'Verify'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get registration data for a specific step
|
// Navigate to the next step
|
||||||
T? getStepData<T>() {
|
void nextStep() async {
|
||||||
switch (T) {
|
// Validate the current form first
|
||||||
case PersonalInfoData:
|
if (formKey?.currentState?.validate() ?? false) {
|
||||||
return registrationData.value.personalInfo as T?;
|
// If this is the last step, submit the form
|
||||||
case IdCardVerificationData:
|
if (currentStep.value == totalSteps - 1) {
|
||||||
return registrationData.value.idCardVerification as T?;
|
await submitRegistration();
|
||||||
case SelfieVerificationData:
|
} else {
|
||||||
return registrationData.value.selfieVerification as T?;
|
// Otherwise, go to the next step
|
||||||
case IdentityVerificationData:
|
|
||||||
return registrationData.value.identityVerification as T?;
|
|
||||||
case OfficerInfoData:
|
|
||||||
return registrationData.value.officerInfo as T?;
|
|
||||||
case UnitInfoData:
|
|
||||||
return registrationData.value.unitInfo as T?;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update specific step data
|
|
||||||
void updateStepData<T>(T data) {
|
|
||||||
switch (T) {
|
|
||||||
case PersonalInfoData:
|
|
||||||
registrationData.value = registrationData.value.copyWith(
|
|
||||||
personalInfo: data as PersonalInfoData,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case IdCardVerificationData:
|
|
||||||
registrationData.value = registrationData.value.copyWith(
|
|
||||||
idCardVerification: data as IdCardVerificationData,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case SelfieVerificationData:
|
|
||||||
registrationData.value = registrationData.value.copyWith(
|
|
||||||
selfieVerification: data as SelfieVerificationData,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case IdentityVerificationData:
|
|
||||||
registrationData.value = registrationData.value.copyWith(
|
|
||||||
identityVerification: data as IdentityVerificationData,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case OfficerInfoData:
|
|
||||||
registrationData.value = registrationData.value.copyWith(
|
|
||||||
officerInfo: data as OfficerInfoData,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case UnitInfoData:
|
|
||||||
registrationData.value = registrationData.value.copyWith(
|
|
||||||
unitInfo: data as UnitInfoData,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate current step
|
|
||||||
bool validateCurrentStep() {
|
|
||||||
switch (currentStep.value) {
|
|
||||||
case 0:
|
|
||||||
return personalInfoController.validate(formKey);
|
|
||||||
case 1:
|
|
||||||
return idCardVerificationController.validate();
|
|
||||||
case 2:
|
|
||||||
return selfieVerificationController.isMatchWithIDCard.value;
|
|
||||||
case 3:
|
|
||||||
return selectedRole.value?.isOfficer == true
|
|
||||||
? officerInfoController!.validate(formKey)
|
|
||||||
: identityController.validate(formKey);
|
|
||||||
case 4:
|
|
||||||
return selectedRole.value?.isOfficer == true
|
|
||||||
? unitInfoController!.validate(formKey)
|
|
||||||
: true;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to next step
|
|
||||||
void nextStep() {
|
|
||||||
// Special case for step 1 (ID Card step)
|
|
||||||
if (currentStep.value == 1) {
|
|
||||||
// Log step status
|
|
||||||
Logger().d(
|
|
||||||
'ID Card step: confirmStatus=${idCardVerificationController.hasConfirmedIdCard.value}',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure ID card is confirmed before allowing to proceed
|
|
||||||
if (!idCardVerificationController.hasConfirmedIdCard.value) {
|
|
||||||
// Show a message that user needs to confirm the ID card first
|
|
||||||
TLoaders.errorSnackBar(
|
|
||||||
title: 'Action Required',
|
|
||||||
message: 'Please confirm your ID card image before proceeding.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass data and proceed
|
|
||||||
// passIdCardDataToNextStep();
|
|
||||||
currentStep.value++; // Directly increment step
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Special case for step 2 (Selfie Verification step)
|
|
||||||
else if (currentStep.value == 2) {
|
|
||||||
// Log step status
|
|
||||||
Logger().d(
|
|
||||||
'Selfie step: confirmStatus=${selfieVerificationController.hasConfirmedSelfie.value}',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure selfie is confirmed before allowing to proceed
|
|
||||||
if (!selfieVerificationController.hasConfirmedSelfie.value) {
|
|
||||||
// Show a message that user needs to confirm the selfie first
|
|
||||||
TLoaders.errorSnackBar(
|
|
||||||
title: 'Action Required',
|
|
||||||
message: 'Please confirm your selfie image before proceeding.',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed to next step
|
|
||||||
currentStep.value++; // Directly increment step
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all other steps, perform standard validation
|
|
||||||
if (!validateCurrentStep()) return;
|
|
||||||
|
|
||||||
// Proceed to next step
|
|
||||||
if (currentStep.value < totalSteps - 1) {
|
|
||||||
// Fixed missing parenthesis
|
|
||||||
currentStep.value++;
|
currentStep.value++;
|
||||||
} else {
|
}
|
||||||
// submitForm();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearPreviousStepErrors() {
|
// Navigate to the previous step
|
||||||
switch (currentStep.value) {
|
|
||||||
case 0:
|
|
||||||
personalInfoController.clearErrors();
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
idCardVerificationController.clearErrors();
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
selfieVerificationController.clearErrors();
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
if (selectedRole.value?.isOfficer == true) {
|
|
||||||
officerInfoController?.clearErrors();
|
|
||||||
} else {
|
|
||||||
identityController.clearErrors();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to previous step
|
|
||||||
void previousStep() {
|
void previousStep() {
|
||||||
if (currentStep.value > 0) {
|
if (currentStep.value > 0) {
|
||||||
// Clear previous step errors
|
|
||||||
clearPreviousStepErrors();
|
|
||||||
|
|
||||||
// Decrement step
|
|
||||||
currentStep.value--;
|
currentStep.value--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Go to specific step
|
// Go to a specific step
|
||||||
void goToStep(int step) {
|
void goToStep(int step) {
|
||||||
if (step >= 0 && step < totalSteps) {
|
if (step >= 0 && step < totalSteps) {
|
||||||
// Only allow going to a step if all previous steps are valid
|
currentStep.value = step;
|
||||||
bool canProceed = true;
|
|
||||||
for (int i = 0; i < step; i++) {
|
|
||||||
currentStep.value = i;
|
|
||||||
if (!validateCurrentStep()) {
|
|
||||||
canProceed = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canProceed) {
|
// Submit registration data
|
||||||
currentStep.value = step;
|
Future<bool> submitRegistration() async {
|
||||||
}
|
isLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Your registration submission logic here
|
||||||
|
await Future.delayed(const Duration(seconds: 2)); // Simulate API call
|
||||||
|
|
||||||
|
// Handle successful registration
|
||||||
|
isLoading.value = false;
|
||||||
|
// Navigate to success page or home page
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
isLoading.value = false;
|
||||||
|
// Handle error
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -409,17 +409,7 @@ class IdCardVerificationController extends GetxController {
|
||||||
if (isIdCardValid.value) {
|
if (isIdCardValid.value) {
|
||||||
hasConfirmedIdCard.value = true;
|
hasConfirmedIdCard.value = true;
|
||||||
|
|
||||||
// Log storage data for debugging
|
clearErrors(); // Clear any previous errors
|
||||||
SharedPreferences.getInstance().then((prefs) {
|
|
||||||
Logger().i('Storage check on confirmation:');
|
|
||||||
Logger().i(
|
|
||||||
'OCR results: ${prefs.getString(_kOcrResultsKey)?.substring(0, 50)}...',
|
|
||||||
);
|
|
||||||
Logger().i(
|
|
||||||
'OCR model: ${prefs.getString(_kOcrModelKey)?.substring(0, 50)}...',
|
|
||||||
);
|
|
||||||
Logger().i('ID card type: ${prefs.getString(_kIdCardTypeKey)}');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -62,16 +62,37 @@ class OfficerInfoController extends GetxController {
|
||||||
Rx<DateTime?> selectedValidUntil = Rx<DateTime?>(null);
|
Rx<DateTime?> selectedValidUntil = Rx<DateTime?>(null);
|
||||||
Rx<DateTime?> selectedDateOfBirth = Rx<DateTime?>(null);
|
Rx<DateTime?> selectedDateOfBirth = Rx<DateTime?>(null);
|
||||||
|
|
||||||
|
// Dark mode reactive state
|
||||||
|
final Rx<bool> isDarkMode = false.obs;
|
||||||
|
|
||||||
|
final isDark = Get.isDarkMode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
|
// Initialize isDarkMode with current value
|
||||||
|
_updateThemeMode();
|
||||||
|
|
||||||
initRepositories();
|
initRepositories();
|
||||||
|
|
||||||
// Fetch units after ensuring repositories are set up
|
// Fetch units after ensuring repositories are set up
|
||||||
getAvailableUnits();
|
getAvailableUnits();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update theme mode based on current Get.isDarkMode value
|
||||||
|
void _updateThemeMode() {
|
||||||
|
// Check the brightness of the current theme
|
||||||
|
final brightness = Get.theme.brightness;
|
||||||
|
isDarkMode.value = brightness == Brightness.dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to check dark mode that can be called from UI
|
||||||
|
bool checkIsDarkMode(BuildContext context) {
|
||||||
|
final brightness = Theme.of(context).brightness;
|
||||||
|
return brightness == Brightness.dark;
|
||||||
|
}
|
||||||
|
|
||||||
void initRepositories() {
|
void initRepositories() {
|
||||||
// Check if repositories are already registered with GetX
|
// Check if repositories are already registered with GetX
|
||||||
unitRepository = Get.find<UnitRepository>();
|
unitRepository = Get.find<UnitRepository>();
|
||||||
|
@ -172,6 +193,48 @@ class OfficerInfoController extends GetxController {
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rankController.text.isEmpty) {
|
||||||
|
rankError.value = 'Rank is required';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (positionController.text.isEmpty) {
|
||||||
|
positionError.value = 'Position is required';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (phoneController.text.isEmpty) {
|
||||||
|
phoneError.value = 'Phone number is required';
|
||||||
|
isValid = false;
|
||||||
|
} else if (!RegExp(r'^\+?[0-9]{10,15}$').hasMatch(phoneController.text)) {
|
||||||
|
phoneError.value = 'Invalid phone number format';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date of Birth validation
|
||||||
|
if (dateOfBirthController.text.isEmpty) {
|
||||||
|
dateOfBirthError.value = 'Date of birth is required';
|
||||||
|
isValid = false;
|
||||||
|
} else if (selectedDateOfBirth.value == null) {
|
||||||
|
dateOfBirthError.value = 'Please select a valid birth date';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valid Until validation
|
||||||
|
if (validUntilController.text.isEmpty) {
|
||||||
|
validUntilError.value = 'Valid until date is required';
|
||||||
|
isValid = false;
|
||||||
|
} else if (selectedValidUntil.value == null) {
|
||||||
|
validUntilError.value = 'Please select a valid date';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placeOfBirthController.text.isEmpty) {
|
||||||
|
placeOfBirthError.value = 'Place of birth is required';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unit selection validation - fixed logic
|
||||||
if (unitIdController.text.isEmpty) {
|
if (unitIdController.text.isEmpty) {
|
||||||
unitIdError.value = 'Please select a unit';
|
unitIdError.value = 'Please select a unit';
|
||||||
isValid = false;
|
isValid = false;
|
||||||
|
@ -181,8 +244,21 @@ class OfficerInfoController extends GetxController {
|
||||||
return isValid;
|
return isValid;
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateOfficer(OfficerModel officer, UserMetadataModel metadata) async {
|
void submitRegistration(
|
||||||
|
OfficerInfoController controller,
|
||||||
|
// FormRegistrationController mainController,
|
||||||
|
GlobalKey<FormState> formKey,
|
||||||
|
) async {
|
||||||
try {
|
try {
|
||||||
|
// First validate the form before showing the loading dialog
|
||||||
|
if (!validate(formKey)) {
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Validation Error',
|
||||||
|
message: 'Please fix the errors in the form before submitting.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TCircularFullScreenLoader.openLoadingDialog();
|
TCircularFullScreenLoader.openLoadingDialog();
|
||||||
|
|
||||||
final isConnected = await NetworkManager.instance.isConnected();
|
final isConnected = await NetworkManager.instance.isConnected();
|
||||||
|
@ -195,49 +271,57 @@ class OfficerInfoController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the form before proceeding
|
// No need to validate again, already done above
|
||||||
if (!validate(null)) {
|
final userId = AuthenticationRepository.instance.authUser!.id;
|
||||||
|
|
||||||
|
final roleId =
|
||||||
|
AuthenticationRepository.instance.authUser!.userMetadata!['role_id'];
|
||||||
|
|
||||||
|
// Check if the user has a valid role
|
||||||
|
if (roleId == null || roleId.isEmpty) {
|
||||||
TLoaders.errorSnackBar(
|
TLoaders.errorSnackBar(
|
||||||
title: 'Validation Error',
|
title: 'Role Error',
|
||||||
message: 'Please fix the errors in the form before submitting.',
|
message: 'User does not have a valid role assigned.',
|
||||||
);
|
);
|
||||||
TCircularFullScreenLoader.stopLoading();
|
TCircularFullScreenLoader.stopLoading();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = officer.copyWith(
|
OfficerModel officer = await OfficerRepository.instance.getOfficerById(
|
||||||
nrp: nrpController.text,
|
userId,
|
||||||
name: nameController.text,
|
|
||||||
rank: rankController.text,
|
|
||||||
position: positionController.text,
|
|
||||||
phone: phoneController.text,
|
|
||||||
unitId: unitIdController.text,
|
|
||||||
validUntil: selectedValidUntil.value,
|
|
||||||
placeOfBirth: placeOfBirthController.text,
|
|
||||||
dateOfBirth: selectedDateOfBirth.value,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Logger().i('Updating officer with data: ${data.toJson()}');
|
// Convert Map<String, dynamic> to UserMetadataModel
|
||||||
|
|
||||||
|
// Create a new OfficerModel instance with the provided data
|
||||||
|
final updateOfficer = officer.copyWith(
|
||||||
|
unitId: unitIdController.text.trim(),
|
||||||
|
nrp: nrpController.text.trim(),
|
||||||
|
name: nameController.text.trim(),
|
||||||
|
rank: rankController.text.trim(),
|
||||||
|
position: positionController.text.trim(),
|
||||||
|
phone: phoneController.text.trim(),
|
||||||
|
placeOfBirth: placeOfBirthController.text.trim(),
|
||||||
|
dateOfBirth: selectedDateOfBirth.value,
|
||||||
|
validUntil: selectedValidUntil.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Logger().i('Updating officer with data: ${updateOfficer.toJson()}');
|
||||||
|
|
||||||
// final updatedOfficer = await OfficerRepository.instance.updateOfficer(
|
final updatedOfficer = await OfficerRepository.instance.updateOfficer(
|
||||||
// data,
|
updateOfficer,
|
||||||
// );
|
);
|
||||||
|
|
||||||
// if (updatedOfficer == null) {
|
if (updatedOfficer == null) {
|
||||||
// TLoaders.errorSnackBar(
|
TLoaders.errorSnackBar(
|
||||||
// title: 'Update Failed',
|
title: 'Update Failed',
|
||||||
// message: 'Failed to update officer information. Please try again.',
|
message: 'Failed to update officer information. Please try again.',
|
||||||
// );
|
);
|
||||||
// TCircularFullScreenLoader.stopLoading();
|
TCircularFullScreenLoader.stopLoading();
|
||||||
// return;
|
return;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// final userMetadata =
|
await UserRepository.instance.updateProfileStatus('completed');
|
||||||
// metadata.copyWith(profileStatus: 'completed').toAuthMetadataJson();
|
|
||||||
|
|
||||||
// await UserRepository.instance.updateUserMetadata(userMetadata);
|
|
||||||
|
|
||||||
// TLoaders.successSnackBar(
|
// TLoaders.successSnackBar(
|
||||||
// title: 'Update Successful',
|
// title: 'Update Successful',
|
||||||
|
@ -245,18 +329,22 @@ class OfficerInfoController extends GetxController {
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// resetForm();
|
// resetForm();
|
||||||
// TCircularFullScreenLoader.stopLoading();
|
TCircularFullScreenLoader.stopLoading();
|
||||||
|
|
||||||
// Get.off(
|
Get.off(
|
||||||
// () => StateScreen(
|
() => StateScreen(
|
||||||
// title: 'Officer Information Created',
|
title: 'Officer Information Created',
|
||||||
// subtitle: 'Officer information has been successfully create.',
|
subtitle: 'Officer information has been successfully created.',
|
||||||
// primaryButtonTitle: 'Back to signin',
|
primaryButtonTitle: 'Back to signin',
|
||||||
// image: TImages.womanHuggingEarth,
|
image:
|
||||||
// showButton: true,
|
isDarkMode.value
|
||||||
// onPressed: () => AuthenticationRepository.instance.screenRedirect(),
|
? TImages.womanHuggingEarthDark
|
||||||
// ),
|
: TImages.womanHuggingEarth,
|
||||||
// );
|
isSvg: true,
|
||||||
|
showButton: true,
|
||||||
|
onPressed: () => AuthenticationRepository.instance.screenRedirect(),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.e('Error updating officer: $e');
|
logger.e('Error updating officer: $e');
|
||||||
TCircularFullScreenLoader.stopLoading();
|
TCircularFullScreenLoader.stopLoading();
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:sigap/src/features/auth/presentasion/pages/signup/step/identity-
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/personal-information/personal_info_step.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/personal-information/personal_info_step.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-verification/selfie_verification_step.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
|
||||||
import 'package:sigap/src/shared/widgets/indicators/step_indicator/step_indicator.dart';
|
import 'package:sigap/src/shared/widgets/indicators/step_indicator/step_indicator.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';
|
||||||
|
@ -67,9 +66,6 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Navigation buttons
|
|
||||||
_buildNavigationButtons(controller),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -108,53 +104,24 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildNavigationButtons(FormRegistrationController controller) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// Back button
|
|
||||||
Obx(
|
|
||||||
() =>
|
|
||||||
controller.currentStep.value > 0
|
|
||||||
? Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(right: TSizes.sm),
|
|
||||||
child: AuthButton(
|
|
||||||
text: 'Previous',
|
|
||||||
onPressed: controller.previousStep,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Next/Submit button
|
|
||||||
Expanded(
|
|
||||||
child: Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
left: controller.currentStep.value > 0 ? TSizes.sm : 0.0,
|
|
||||||
),
|
|
||||||
child: Obx(
|
|
||||||
() => AuthButton(
|
|
||||||
text:
|
|
||||||
controller.currentStep.value == controller.totalSteps - 1
|
|
||||||
? 'Submit'
|
|
||||||
: 'Next',
|
|
||||||
onPressed: controller.nextStep,
|
|
||||||
isLoading: controller.isLoading.value,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildStepContent(FormRegistrationController controller) {
|
Widget _buildStepContent(FormRegistrationController controller) {
|
||||||
final isOfficer = controller.selectedRole.value?.isOfficer ?? false;
|
final isOfficer = controller.selectedRole.value?.isOfficer ?? false;
|
||||||
|
|
||||||
|
// Different step content for officer vs viewer
|
||||||
|
if (isOfficer) {
|
||||||
|
// Officer registration flow (3 steps)
|
||||||
|
switch (controller.currentStep.value) {
|
||||||
|
case 0:
|
||||||
|
return const IdCardVerificationStep();
|
||||||
|
case 1:
|
||||||
|
return const SelfieVerificationStep();
|
||||||
|
case 2:
|
||||||
|
return const OfficerInfoStep();
|
||||||
|
default:
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Viewer registration flow (4 steps)
|
||||||
switch (controller.currentStep.value) {
|
switch (controller.currentStep.value) {
|
||||||
case 0:
|
case 0:
|
||||||
return const PersonalInfoStep();
|
return const PersonalInfoStep();
|
||||||
|
@ -163,11 +130,10 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
case 2:
|
case 2:
|
||||||
return const SelfieVerificationStep();
|
return const SelfieVerificationStep();
|
||||||
case 3:
|
case 3:
|
||||||
return isOfficer
|
return const IdentityVerificationStep();
|
||||||
? const OfficerInfoStep()
|
|
||||||
: const IdentityVerificationStep();
|
|
||||||
default:
|
default:
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/controllers/role_selection_controller.dart';
|
|
||||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
|
|
|
@ -8,6 +8,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id-
|
||||||
import 'package:sigap/src/shared/widgets/image_upload/image_source_dialog.dart';
|
import 'package:sigap/src/shared/widgets/image_upload/image_source_dialog.dart';
|
||||||
import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart';
|
import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart';
|
||||||
import 'package:sigap/src/shared/widgets/info/tips_container.dart';
|
import 'package:sigap/src/shared/widgets/info/tips_container.dart';
|
||||||
|
import 'package:sigap/src/shared/widgets/navigation/step_navigation_buttons.dart';
|
||||||
import 'package:sigap/src/shared/widgets/verification/ocr_result_card.dart';
|
import 'package:sigap/src/shared/widgets/verification/ocr_result_card.dart';
|
||||||
import 'package:sigap/src/shared/widgets/verification/validation_message_card.dart';
|
import 'package:sigap/src/shared/widgets/verification/validation_message_card.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
@ -146,11 +147,50 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
// Tips Section
|
// Tips Section
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
_buildIdCardTips(idCardType),
|
_buildIdCardTips(idCardType),
|
||||||
|
|
||||||
|
// Add space before navigation buttons
|
||||||
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
|
// Navigation buttons with loading state
|
||||||
|
Obx(
|
||||||
|
() => StepNavigationButtons(
|
||||||
|
showPrevious: mainController.currentStep.value > 0,
|
||||||
|
isLastStep: false,
|
||||||
|
onPrevious: mainController.previousStep,
|
||||||
|
onNext: () => _handleNextStep(controller, mainController),
|
||||||
|
isLoading:
|
||||||
|
controller.isVerifying.value ||
|
||||||
|
controller.isUploadingIdCard.value,
|
||||||
|
errorMessage: controller.idCardError.value,
|
||||||
|
nextButtonText: 'Continue',
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a method to handle the next step validation
|
||||||
|
void _handleNextStep(
|
||||||
|
IdCardVerificationController controller,
|
||||||
|
FormRegistrationController mainController,
|
||||||
|
) {
|
||||||
|
// Validate that ID card is uploaded and verified
|
||||||
|
if (controller.idCardImage.value == null) {
|
||||||
|
controller.idCardError.value = 'Please upload your ID card to continue';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!controller.hasConfirmedIdCard.value) {
|
||||||
|
controller.idCardError.value =
|
||||||
|
'Your ID card must be confirmed before going to the next step';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If everything is valid, go to next step
|
||||||
|
mainController.nextStep();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildHeader(BuildContext context, String idCardType) {
|
Widget _buildHeader(BuildContext context, String idCardType) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/reg
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart';
|
||||||
import 'package:sigap/src/shared/widgets/form/form_section_header.dart';
|
import 'package:sigap/src/shared/widgets/form/form_section_header.dart';
|
||||||
|
import 'package:sigap/src/shared/widgets/navigation/step_navigation_buttons.dart';
|
||||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||||
import 'package:sigap/src/utils/constants/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';
|
||||||
|
@ -20,7 +21,9 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
final mainController = Get.find<FormRegistrationController>();
|
final mainController = Get.find<FormRegistrationController>();
|
||||||
mainController.formKey = formKey;
|
mainController.formKey = formKey;
|
||||||
|
|
||||||
// Check if KTA data exists and populate fields if available
|
final isSubmitting = false.obs;
|
||||||
|
final submissionError = ''.obs;
|
||||||
|
|
||||||
_populateFieldsFromKta(
|
_populateFieldsFromKta(
|
||||||
controller,
|
controller,
|
||||||
mainController.idCardVerificationController,
|
mainController.idCardVerificationController,
|
||||||
|
@ -53,7 +56,6 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.nrpError.value = '';
|
controller.nrpError.value = '';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildErrorText(controller.nrpError),
|
|
||||||
|
|
||||||
// Name field
|
// Name field
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
|
@ -74,7 +76,6 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.nameError.value = '';
|
controller.nameError.value = '';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildErrorText(controller.nameError),
|
|
||||||
|
|
||||||
// Rank field
|
// Rank field
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
|
@ -89,10 +90,10 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.rankError.value = '';
|
controller.rankError.value = '';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildErrorText(controller.rankError),
|
|
||||||
|
|
||||||
// Position field
|
// Position field
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
|
|
||||||
label: 'Position',
|
label: 'Position',
|
||||||
controller: controller.positionController,
|
controller: controller.positionController,
|
||||||
validator: (v) => TValidators.validateUserInput('Position', v, 100),
|
validator: (v) => TValidators.validateUserInput('Position', v, 100),
|
||||||
|
@ -103,8 +104,8 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.positionController.text = value;
|
controller.positionController.text = value;
|
||||||
controller.positionError.value = '';
|
controller.positionError.value = '';
|
||||||
},
|
},
|
||||||
|
|
||||||
),
|
),
|
||||||
_buildErrorText(controller.positionError),
|
|
||||||
|
|
||||||
// Phone field
|
// Phone field
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
|
@ -120,7 +121,6 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.phoneError.value = '';
|
controller.phoneError.value = '';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildErrorText(controller.phoneError),
|
|
||||||
|
|
||||||
// Place of Birth field
|
// Place of Birth field
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
|
@ -136,7 +136,6 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.placeOfBirthError.value = '';
|
controller.placeOfBirthError.value = '';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_buildErrorText(controller.placeOfBirthError),
|
|
||||||
|
|
||||||
// Date of Birth field
|
// Date of Birth field
|
||||||
_buildDateField(
|
_buildDateField(
|
||||||
|
@ -145,15 +144,10 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
label: 'Date of Birth',
|
label: 'Date of Birth',
|
||||||
textController: controller.dateOfBirthController,
|
textController: controller.dateOfBirthController,
|
||||||
errorValue: controller.dateOfBirthError,
|
errorValue: controller.dateOfBirthError,
|
||||||
hintText: 'YYYY-MM-DD',
|
hintText: 'Select your birth date',
|
||||||
initialDate: DateTime.now().subtract(
|
dateType: DateFieldType.birthDate,
|
||||||
const Duration(days: 365 * 18),
|
|
||||||
), // Default to 18 years ago
|
|
||||||
firstDate: DateTime(1950),
|
|
||||||
lastDate: DateTime.now(),
|
|
||||||
onDateSelected: controller.setDateOfBirth,
|
onDateSelected: controller.setDateOfBirth,
|
||||||
),
|
),
|
||||||
_buildErrorText(controller.dateOfBirthError),
|
|
||||||
|
|
||||||
// Valid Until field
|
// Valid Until field
|
||||||
_buildDateField(
|
_buildDateField(
|
||||||
|
@ -162,15 +156,10 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
label: 'Valid Until',
|
label: 'Valid Until',
|
||||||
textController: controller.validUntilController,
|
textController: controller.validUntilController,
|
||||||
errorValue: controller.validUntilError,
|
errorValue: controller.validUntilError,
|
||||||
hintText: 'YYYY-MM-DD',
|
hintText: 'Select expiry date',
|
||||||
initialDate: DateTime.now().add(
|
dateType: DateFieldType.validUntil,
|
||||||
const Duration(days: 365),
|
|
||||||
), // Default to 1 year from now
|
|
||||||
firstDate: DateTime.now(),
|
|
||||||
lastDate: DateTime(2100),
|
|
||||||
onDateSelected: controller.setValidUntilDate,
|
onDateSelected: controller.setValidUntilDate,
|
||||||
),
|
),
|
||||||
_buildErrorText(controller.validUntilError),
|
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwSections),
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
|
@ -182,8 +171,9 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
// Unit dropdown
|
// Unit dropdown with error styling
|
||||||
_buildUnitDropdown(
|
_buildUnitDropdown(
|
||||||
|
context,
|
||||||
controller,
|
controller,
|
||||||
mainController.idCardVerificationController,
|
mainController.idCardVerificationController,
|
||||||
),
|
),
|
||||||
|
@ -225,41 +215,66 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
|
// Navigation buttons
|
||||||
|
Obx(
|
||||||
|
() => StepNavigationButtons(
|
||||||
|
showPrevious: true,
|
||||||
|
isLastStep: true,
|
||||||
|
onPrevious: mainController.previousStep,
|
||||||
|
onNext: () => controller.submitRegistration(controller, formKey),
|
||||||
|
isLoading: isSubmitting.value,
|
||||||
|
errorMessage: submissionError.value,
|
||||||
|
nextButtonText: 'Submit Registration',
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Populates form fields from KTA data if available
|
|
||||||
void _populateFieldsFromKta(
|
void _populateFieldsFromKta(
|
||||||
OfficerInfoController controller,
|
OfficerInfoController controller,
|
||||||
IdCardVerificationController mainController,
|
IdCardVerificationController mainController,
|
||||||
) {
|
) {
|
||||||
// Check if KTA data exists in the main controller
|
|
||||||
final KtaModel? ktaData = mainController.ktaModel.value;
|
final KtaModel? ktaData = mainController.ktaModel.value;
|
||||||
|
|
||||||
if (ktaData != null) {
|
if (ktaData != null) {
|
||||||
controller.populateFromKta(ktaData);
|
controller.populateFromKta(ktaData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to build error text consistently
|
// error text with better styling
|
||||||
Widget _buildErrorText(RxString errorValue) {
|
Widget _buildErrorText(RxString errorValue) {
|
||||||
return Obx(
|
return Obx(
|
||||||
() =>
|
() =>
|
||||||
errorValue.value.isNotEmpty
|
errorValue.value.isNotEmpty
|
||||||
? Padding(
|
? Container(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
margin: const EdgeInsets.only(top: 6, left: 4),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, size: 14, color: TColors.error),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
errorValue.value,
|
errorValue.value,
|
||||||
style: TextStyle(color: Colors.red[700], fontSize: 12),
|
style: TextStyle(
|
||||||
|
color: TColors.error,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build date picker field
|
// date picker with proper date ranges and error styling
|
||||||
Widget _buildDateField({
|
Widget _buildDateField({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
required OfficerInfoController controller,
|
required OfficerInfoController controller,
|
||||||
|
@ -267,36 +282,85 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
required TextEditingController textController,
|
required TextEditingController textController,
|
||||||
required RxString errorValue,
|
required RxString errorValue,
|
||||||
required String hintText,
|
required String hintText,
|
||||||
required DateTime initialDate,
|
required DateFieldType dateType,
|
||||||
required DateTime firstDate,
|
|
||||||
required DateTime lastDate,
|
|
||||||
required Function(DateTime) onDateSelected,
|
required Function(DateTime) onDateSelected,
|
||||||
}) {
|
}) {
|
||||||
|
return Obx(() {
|
||||||
|
final hasError = errorValue.value.isNotEmpty;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
// Define date ranges based on type
|
||||||
|
DateTime initialDate;
|
||||||
|
DateTime firstDate;
|
||||||
|
DateTime lastDate;
|
||||||
|
|
||||||
|
switch (dateType) {
|
||||||
|
case DateFieldType.birthDate:
|
||||||
|
// For birth date: past dates only
|
||||||
|
final now = DateTime.now();
|
||||||
|
// Set initialDate to 25 years ago, allow dates from 1940 up to today (no future dates)
|
||||||
|
initialDate = DateTime(
|
||||||
|
now.year - 25,
|
||||||
|
now.month,
|
||||||
|
now.day,
|
||||||
|
); // Default to 25 years ago
|
||||||
|
firstDate = DateTime(1940); // Allow very old dates
|
||||||
|
lastDate = DateTime(
|
||||||
|
now.year,
|
||||||
|
now.month,
|
||||||
|
now.day,
|
||||||
|
); // Today (no future)
|
||||||
|
break;
|
||||||
|
case DateFieldType.validUntil:
|
||||||
|
// For valid until: future dates only
|
||||||
|
final now = DateTime.now();
|
||||||
|
initialDate = DateTime(
|
||||||
|
now.year + 1,
|
||||||
|
now.month,
|
||||||
|
now.day,
|
||||||
|
); // Default to 1 year from now
|
||||||
|
firstDate = now; // Start from today
|
||||||
|
lastDate = DateTime(2100); // Far future
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
CustomTextField(
|
// Label
|
||||||
label: label,
|
Text(
|
||||||
controller: textController,
|
label,
|
||||||
readOnly: true, // Make read-only since we use date picker
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
errorText: errorValue.value,
|
fontWeight: FontWeight.w500,
|
||||||
hintText: hintText,
|
color: hasError ? TColors.error : null,
|
||||||
suffixIcon: IconButton(
|
),
|
||||||
icon: const Icon(Icons.calendar_today),
|
),
|
||||||
onPressed: () async {
|
const SizedBox(height: TSizes.sm),
|
||||||
|
|
||||||
|
// Date field container
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () async {
|
||||||
final date = await showDatePicker(
|
final date = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: initialDate,
|
initialDate:
|
||||||
|
textController.text.isNotEmpty
|
||||||
|
? DateTime.tryParse(textController.text) ?? initialDate
|
||||||
|
: initialDate,
|
||||||
firstDate: firstDate,
|
firstDate: firstDate,
|
||||||
lastDate: lastDate,
|
lastDate: lastDate,
|
||||||
|
helpText:
|
||||||
|
dateType == DateFieldType.birthDate
|
||||||
|
? 'Select your birth date'
|
||||||
|
: 'Select expiry date',
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
return Theme(
|
return Theme(
|
||||||
data: Theme.of(context).copyWith(
|
data: theme.copyWith(
|
||||||
colorScheme: ColorScheme.light(
|
colorScheme: theme.colorScheme.copyWith(
|
||||||
primary: Theme.of(context).primaryColor,
|
primary: isDark ? TColors.accent : TColors.primary,
|
||||||
onPrimary: Colors.white,
|
onPrimary: isDark ? TColors.primary : TColors.accent,
|
||||||
surface: Colors.white,
|
surface: isDark ? Colors.grey[800] : TColors.accent,
|
||||||
onSurface: Colors.black,
|
onSurface: isDark ? TColors.accent : Colors.black,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: child!,
|
child: child!,
|
||||||
|
@ -306,50 +370,114 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
|
|
||||||
if (date != null) {
|
if (date != null) {
|
||||||
onDateSelected(date);
|
onDateSelected(date);
|
||||||
|
errorValue.value = ''; // Clear error when date is selected
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: TSizes.md,
|
||||||
|
vertical: TSizes.md,
|
||||||
),
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
hasError
|
||||||
|
? TColors.error
|
||||||
|
: (isDark ? Colors.grey[600]! : Colors.grey[300]!),
|
||||||
|
width: hasError ? 2 : 1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
|
color: (isDark ? TColors.dark : TColors.lightContainer),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.calendar_today,
|
||||||
|
color:
|
||||||
|
hasError
|
||||||
|
? TColors.error
|
||||||
|
: (isDark ? Colors.grey[400] : Colors.grey[600]),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
textController.text.isNotEmpty
|
||||||
|
? _formatDisplayDate(textController.text)
|
||||||
|
: hintText,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color:
|
||||||
|
(hasError
|
||||||
|
? TColors.error
|
||||||
|
: theme.textTheme.bodyMedium?.color),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Icon(
|
||||||
|
Icons.keyboard_arrow_down,
|
||||||
|
color:
|
||||||
|
hasError
|
||||||
|
? TColors.error
|
||||||
|
: (isDark ? Colors.grey[400] : Colors.grey[600]),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Error message
|
||||||
|
_buildErrorText(errorValue),
|
||||||
|
|
||||||
|
// Helper text for date range
|
||||||
|
if (!hasError && textController.text.isEmpty)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 6, left: 4),
|
||||||
|
child: Text(
|
||||||
|
dateType == DateFieldType.birthDate
|
||||||
|
? 'Select a date from the past'
|
||||||
|
: 'Select a future date for document expiry',
|
||||||
|
style: TextStyle(
|
||||||
|
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwInputFields),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build unit dropdown selection
|
// unit dropdown with error styling
|
||||||
Widget _buildUnitDropdown(
|
Widget _buildUnitDropdown(
|
||||||
|
BuildContext context,
|
||||||
OfficerInfoController controller,
|
OfficerInfoController controller,
|
||||||
IdCardVerificationController idCardController,
|
IdCardVerificationController idCardController,
|
||||||
) {
|
) {
|
||||||
|
return Obx(() {
|
||||||
|
final hasError = controller.unitIdError.value.isNotEmpty;
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
final borderRadius = BorderRadius.circular(TSizes.inputFieldRadius);
|
||||||
|
final fillColor = isDark ? TColors.dark : TColors.accent;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Label - using context directly
|
// Label
|
||||||
Builder(
|
Text(
|
||||||
builder: (context) {
|
|
||||||
return Text(
|
|
||||||
'Select Unit:',
|
'Select Unit:',
|
||||||
style: Theme.of(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
context,
|
fontWeight: FontWeight.w500,
|
||||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
|
color: hasError ? TColors.error : null,
|
||||||
);
|
),
|
||||||
},
|
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: TSizes.sm),
|
const SizedBox(height: TSizes.sm),
|
||||||
|
|
||||||
// Dropdown using Builder to access current context (and theme)
|
// Loading state
|
||||||
Builder(
|
if (controller.isLoadingUnits.value)
|
||||||
builder: (context) {
|
Container(
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
|
|
||||||
// Use custom text field styling for consistency
|
|
||||||
final borderRadius = BorderRadius.circular(TSizes.inputFieldRadius);
|
|
||||||
final fillColor = isDark ? TColors.dark : TColors.lightContainer;
|
|
||||||
|
|
||||||
return GetX<OfficerInfoController>(
|
|
||||||
builder: (controller) {
|
|
||||||
if (controller.isLoadingUnits.value) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: TSizes.md,
|
horizontal: TSizes.md,
|
||||||
vertical: TSizes.md,
|
vertical: TSizes.md,
|
||||||
|
@ -362,64 +490,91 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(TSizes.sm),
|
padding: const EdgeInsets.all(TSizes.sm),
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(color: theme.primaryColor),
|
||||||
color: theme.primaryColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
);
|
// No units available
|
||||||
}
|
else if (controller.availableUnits.isEmpty)
|
||||||
|
Container(
|
||||||
if (controller.availableUnits.isEmpty) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: TSizes.md,
|
horizontal: TSizes.md,
|
||||||
vertical: TSizes.md,
|
vertical: TSizes.md,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: theme.dividerColor),
|
border: Border.all(
|
||||||
|
color: hasError ? TColors.error : theme.dividerColor,
|
||||||
|
width: hasError ? 2 : 1,
|
||||||
|
),
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
color: fillColor,
|
color: fillColor,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.warning_outlined,
|
||||||
|
color: hasError ? TColors.error : Colors.orange[600],
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
'No units available',
|
'No units available',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: isDark ? Colors.grey[400] : Colors.grey[600],
|
color: hasError ? TColors.error : Colors.orange[600],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Units available
|
||||||
|
else
|
||||||
|
_buildUnitDropdownContent(
|
||||||
|
controller,
|
||||||
|
idCardController,
|
||||||
|
hasError,
|
||||||
|
theme,
|
||||||
|
isDark,
|
||||||
|
borderRadius,
|
||||||
|
fillColor,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Error message
|
||||||
|
_buildErrorText(controller.unitIdError),
|
||||||
|
|
||||||
|
const SizedBox(height: TSizes.spaceBtwInputFields),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the selected unit (if any)
|
Widget _buildUnitDropdownContent(
|
||||||
|
OfficerInfoController controller,
|
||||||
|
IdCardVerificationController idCardController,
|
||||||
|
bool hasError,
|
||||||
|
ThemeData theme,
|
||||||
|
bool isDark,
|
||||||
|
BorderRadius borderRadius,
|
||||||
|
Color fillColor,
|
||||||
|
) {
|
||||||
|
return GetX<OfficerInfoController>(
|
||||||
|
builder: (controller) {
|
||||||
final selectedUnit = controller.availableUnits.firstWhereOrNull(
|
final selectedUnit = controller.availableUnits.firstWhereOrNull(
|
||||||
(unit) => unit.codeUnit == controller.unitIdController.text,
|
(unit) => unit.codeUnit == controller.unitIdController.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If units are loaded and we have KTA data with a police unit,
|
// Auto-select matching unit from KTA data
|
||||||
// try to find a matching unit and select it
|
|
||||||
if (controller.availableUnits.isNotEmpty &&
|
if (controller.availableUnits.isNotEmpty &&
|
||||||
controller.unitIdController.text.isEmpty) {
|
controller.unitIdController.text.isEmpty) {
|
||||||
final ktaUnit =
|
final ktaUnit = idCardController.ktaModel.value?.policeUnit ?? '';
|
||||||
idCardController.ktaModel.value?.policeUnit ?? '';
|
|
||||||
|
|
||||||
// More flexible matching logic to find the best matching unit
|
|
||||||
final matchingUnit = controller.availableUnits.firstWhereOrNull(
|
final matchingUnit = controller.availableUnits.firstWhereOrNull(
|
||||||
(unit) =>
|
(unit) =>
|
||||||
// Try exact match first
|
|
||||||
unit.name.toLowerCase() == ktaUnit.toLowerCase() ||
|
unit.name.toLowerCase() == ktaUnit.toLowerCase() ||
|
||||||
// Then try contains match
|
unit.name.toLowerCase().contains(ktaUnit.toLowerCase()) ||
|
||||||
unit.name.toLowerCase().contains(
|
|
||||||
ktaUnit.toLowerCase(),
|
|
||||||
) ||
|
|
||||||
// Or if the KTA unit contains the available unit name
|
|
||||||
ktaUnit.toLowerCase().contains(unit.name.toLowerCase()),
|
ktaUnit.toLowerCase().contains(unit.name.toLowerCase()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (matchingUnit != null) {
|
if (matchingUnit != null) {
|
||||||
// Use Future.microtask to avoid setState during build
|
Future.microtask(() => controller.onUnitSelected(matchingUnit));
|
||||||
Future.microtask(
|
|
||||||
() => controller.onUnitSelected(matchingUnit),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -428,8 +583,11 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
// Dropdown Selection Button
|
// Dropdown Selection Button
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Toggle dropdown visibility
|
|
||||||
controller.isUnitDropdownOpen.toggle();
|
controller.isUnitDropdownOpen.toggle();
|
||||||
|
if (hasError) {
|
||||||
|
controller.unitIdError.value =
|
||||||
|
''; // Clear error on interaction
|
||||||
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
@ -439,17 +597,34 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color:
|
color:
|
||||||
controller.isUnitDropdownOpen.value
|
hasError
|
||||||
|
? TColors.error
|
||||||
|
: (controller.isUnitDropdownOpen.value
|
||||||
? theme.primaryColor
|
? theme.primaryColor
|
||||||
: theme.dividerColor,
|
: theme.dividerColor),
|
||||||
width:
|
width:
|
||||||
controller.isUnitDropdownOpen.value ? 1.5 : 1,
|
hasError || controller.isUnitDropdownOpen.value ? 2 : 1,
|
||||||
),
|
),
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
color: fillColor,
|
color: fillColor,
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
Icon(
|
||||||
|
selectedUnit != null
|
||||||
|
? Icons.shield
|
||||||
|
: Icons.shield_outlined,
|
||||||
|
color:
|
||||||
|
hasError
|
||||||
|
? TColors.error
|
||||||
|
: (selectedUnit != null
|
||||||
|
? theme.primaryColor
|
||||||
|
: (isDark
|
||||||
|
? Colors.grey[400]
|
||||||
|
: Colors.grey[600])),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
selectedUnit != null
|
selectedUnit != null
|
||||||
|
@ -457,11 +632,13 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
: 'Select Unit',
|
: 'Select Unit',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color:
|
color:
|
||||||
selectedUnit != null
|
hasError
|
||||||
|
? TColors.error
|
||||||
|
: (selectedUnit != null
|
||||||
? theme.textTheme.bodyMedium?.color
|
? theme.textTheme.bodyMedium?.color
|
||||||
: (isDark
|
: (isDark
|
||||||
? Colors.grey[400]
|
? Colors.grey[400]
|
||||||
: Colors.grey[600]),
|
: Colors.grey[600])),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -470,11 +647,13 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
? Icons.keyboard_arrow_up
|
? Icons.keyboard_arrow_up
|
||||||
: Icons.keyboard_arrow_down,
|
: Icons.keyboard_arrow_down,
|
||||||
color:
|
color:
|
||||||
controller.isUnitDropdownOpen.value
|
hasError
|
||||||
|
? TColors.error
|
||||||
|
: (controller.isUnitDropdownOpen.value
|
||||||
? theme.primaryColor
|
? theme.primaryColor
|
||||||
: (isDark
|
: (isDark
|
||||||
? Colors.grey[400]
|
? Colors.grey[400]
|
||||||
: Colors.grey[600]),
|
: Colors.grey[600])),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -490,14 +669,14 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(
|
color: Colors.black.withOpacity(isDark ? 0.3 : 0.1),
|
||||||
isDark ? 0.3 : 0.1,
|
|
||||||
),
|
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
border: Border.all(color: theme.dividerColor),
|
border: Border.all(
|
||||||
|
color: hasError ? TColors.error : theme.dividerColor,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
constraints: const BoxConstraints(maxHeight: 250),
|
constraints: const BoxConstraints(maxHeight: 250),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
@ -509,13 +688,14 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final unit = controller.availableUnits[index];
|
final unit = controller.availableUnits[index];
|
||||||
final isSelected =
|
final isSelected =
|
||||||
unit.codeUnit ==
|
unit.codeUnit == controller.unitIdController.text;
|
||||||
controller.unitIdController.text;
|
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
controller.onUnitSelected(unit);
|
controller.onUnitSelected(unit);
|
||||||
controller.isUnitDropdownOpen.value = false;
|
controller.isUnitDropdownOpen.value = false;
|
||||||
|
controller.unitIdError.value =
|
||||||
|
''; // Clear error on selection
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
|
@ -530,15 +710,12 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
)
|
)
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
border:
|
border:
|
||||||
index <
|
index < controller.availableUnits.length - 1
|
||||||
controller
|
|
||||||
.availableUnits
|
|
||||||
.length -
|
|
||||||
1
|
|
||||||
? Border(
|
? Border(
|
||||||
bottom: BorderSide(
|
bottom: BorderSide(
|
||||||
color: theme.dividerColor
|
color: theme.dividerColor.withOpacity(
|
||||||
.withOpacity(0.5),
|
0.5,
|
||||||
|
),
|
||||||
width: 0.5,
|
width: 0.5,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -546,7 +723,6 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Unit Icon
|
|
||||||
Icon(
|
Icon(
|
||||||
isSelected
|
isSelected
|
||||||
? Icons.shield
|
? Icons.shield
|
||||||
|
@ -560,20 +736,14 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
// Unit Name
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'${unit.name} (${unit.type.name})',
|
'${unit.name} (${unit.type.name})',
|
||||||
style: theme.textTheme.bodyMedium
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
?.copyWith(
|
|
||||||
color:
|
color:
|
||||||
isSelected
|
isSelected
|
||||||
? theme.primaryColor
|
? theme.primaryColor
|
||||||
: theme
|
: theme.textTheme.bodyMedium?.color,
|
||||||
.textTheme
|
|
||||||
.bodyMedium
|
|
||||||
?.color,
|
|
||||||
fontWeight:
|
fontWeight:
|
||||||
isSelected
|
isSelected
|
||||||
? FontWeight.bold
|
? FontWeight.bold
|
||||||
|
@ -581,8 +751,6 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Checkmark for selected item
|
|
||||||
if (isSelected)
|
if (isSelected)
|
||||||
Icon(
|
Icon(
|
||||||
Icons.check,
|
Icons.check,
|
||||||
|
@ -599,26 +767,18 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
),
|
),
|
||||||
|
|
||||||
// Selected unit display
|
// Selected unit display
|
||||||
if (selectedUnit != null &&
|
if (selectedUnit != null && !controller.isUnitDropdownOpen.value)
|
||||||
!controller.isUnitDropdownOpen.value)
|
|
||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.only(
|
margin: const EdgeInsets.only(top: TSizes.spaceBtwInputFields),
|
||||||
top: TSizes.spaceBtwInputFields,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(TSizes.md),
|
padding: const EdgeInsets.all(TSizes.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.primaryColor.withOpacity(
|
color: theme.primaryColor.withOpacity(isDark ? 0.2 : 0.1),
|
||||||
isDark ? 0.2 : 0.1,
|
|
||||||
),
|
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
border: Border.all(color: theme.primaryColor),
|
border: Border.all(color: theme.primaryColor),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(Icons.shield_outlined, color: theme.primaryColor),
|
||||||
Icons.shield_outlined,
|
|
||||||
color: theme.primaryColor,
|
|
||||||
),
|
|
||||||
const SizedBox(width: TSizes.sm),
|
const SizedBox(width: TSizes.sm),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -628,9 +788,7 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
'Selected Unit',
|
'Selected Unit',
|
||||||
style: theme.textTheme.labelSmall?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color:
|
color:
|
||||||
isDark
|
isDark ? Colors.grey[400] : Colors.grey[600],
|
||||||
? Colors.grey[400]
|
|
||||||
: Colors.grey[600],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
|
@ -650,16 +808,36 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Error message
|
|
||||||
_buildErrorText(controller.unitIdError),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
),
|
|
||||||
],
|
// Helper method to format date for display
|
||||||
);
|
String _formatDisplayDate(String dateString) {
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(dateString);
|
||||||
|
final months = [
|
||||||
|
'Jan',
|
||||||
|
'Feb',
|
||||||
|
'Mar',
|
||||||
|
'Apr',
|
||||||
|
'May',
|
||||||
|
'Jun',
|
||||||
|
'Jul',
|
||||||
|
'Aug',
|
||||||
|
'Sep',
|
||||||
|
'Oct',
|
||||||
|
'Nov',
|
||||||
|
'Dec',
|
||||||
|
];
|
||||||
|
return '${date.day} ${months[date.month - 1]} ${date.year}';
|
||||||
|
} catch (e) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enum for date field types
|
||||||
|
enum DateFieldType { birthDate, validUntil }
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -6,6 +6,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/sel
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/selfie-verification/selfie_verification_controller.dart';
|
||||||
import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart';
|
import 'package:sigap/src/shared/widgets/image_upload/image_uploader.dart';
|
||||||
import 'package:sigap/src/shared/widgets/info/tips_container.dart';
|
import 'package:sigap/src/shared/widgets/info/tips_container.dart';
|
||||||
|
import 'package:sigap/src/shared/widgets/navigation/step_navigation_buttons.dart';
|
||||||
import 'package:sigap/src/shared/widgets/verification/validation_message_card.dart';
|
import 'package:sigap/src/shared/widgets/verification/validation_message_card.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';
|
||||||
|
@ -67,11 +68,53 @@ class SelfieVerificationStep extends StatelessWidget {
|
||||||
|
|
||||||
// Tips container
|
// Tips container
|
||||||
_buildSelfieTips(),
|
_buildSelfieTips(),
|
||||||
|
|
||||||
|
// Add space before navigation buttons
|
||||||
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
|
// Navigation buttons with loading state
|
||||||
|
Obx(
|
||||||
|
() => StepNavigationButtons(
|
||||||
|
showPrevious: true,
|
||||||
|
isLastStep: false,
|
||||||
|
onPrevious: mainController.previousStep,
|
||||||
|
onNext: () => _handleNextStep(controller, mainController),
|
||||||
|
isLoading:
|
||||||
|
controller.isVerifyingFace.value ||
|
||||||
|
controller.isComparingWithIDCard.value ||
|
||||||
|
controller.isPerformingLivenessCheck.value,
|
||||||
|
errorMessage: controller.selfieError.value,
|
||||||
|
nextButtonText: 'Continue',
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a method to handle the next step validation
|
||||||
|
void _handleNextStep(
|
||||||
|
SelfieVerificationController controller,
|
||||||
|
FormRegistrationController mainController,
|
||||||
|
) {
|
||||||
|
// Validate that selfie is taken and verified
|
||||||
|
if (controller.selfieImage.value == null) {
|
||||||
|
controller.selfieError.value =
|
||||||
|
'Please complete selfie verification to continue';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!controller.isMatchWithIDCard.value &&
|
||||||
|
!controller.autoVerifyForDev.value) {
|
||||||
|
controller.selfieError.value =
|
||||||
|
'Your selfie verification must be completed successfully';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If everything is valid, go to next step
|
||||||
|
mainController.nextStep();
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildDevelopmentModeIndicator(FacialVerificationService service) {
|
Widget _buildDevelopmentModeIndicator(FacialVerificationService service) {
|
||||||
if (!service.skipFaceVerification) return const SizedBox.shrink();
|
if (!service.skipFaceVerification) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ class AuthButton extends StatelessWidget {
|
||||||
onPressed: isLoading ? null : onPressed,
|
onPressed: isLoading ? null : onPressed,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: backgroundColor ?? Theme.of(context).primaryColor,
|
backgroundColor: backgroundColor ?? Theme.of(context).primaryColor,
|
||||||
foregroundColor: textColor ?? Colors.white,
|
foregroundColor: textColor ?? Theme.of(context).colorScheme.onPrimary,
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
borderRadius: BorderRadius.circular(TSizes.buttonRadius),
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
||||||
|
|
||||||
class OfficerModel {
|
class OfficerModel {
|
||||||
final String id;
|
final String? id;
|
||||||
final String unitId;
|
final String? unitId;
|
||||||
final String roleId;
|
final String? roleId;
|
||||||
final String? patrolUnitId;
|
final String? patrolUnitId;
|
||||||
final String nrp;
|
final dynamic nrp; // Changed to dynamic to handle both string and int
|
||||||
final String name;
|
final String? name;
|
||||||
final String? rank;
|
final String? rank;
|
||||||
final String? position;
|
final String? position;
|
||||||
final String? phone;
|
final String? phone;
|
||||||
|
@ -19,14 +19,20 @@ class OfficerModel {
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
final DateTime? updatedAt;
|
final DateTime? updatedAt;
|
||||||
final RoleModel? role;
|
final RoleModel? role;
|
||||||
|
// New fields from the JSON response
|
||||||
|
final String? bannedReason;
|
||||||
|
final DateTime? bannedUntil;
|
||||||
|
final bool isBanned;
|
||||||
|
final int panicStrike;
|
||||||
|
final int spoofingAttempts;
|
||||||
|
|
||||||
OfficerModel({
|
OfficerModel({
|
||||||
required this.id,
|
this.id,
|
||||||
required this.unitId,
|
this.unitId,
|
||||||
required this.roleId,
|
this.roleId,
|
||||||
this.patrolUnitId,
|
this.patrolUnitId,
|
||||||
required this.nrp,
|
this.nrp,
|
||||||
required this.name,
|
this.name,
|
||||||
this.rank,
|
this.rank,
|
||||||
this.position,
|
this.position,
|
||||||
this.phone,
|
this.phone,
|
||||||
|
@ -39,23 +45,28 @@ class OfficerModel {
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.updatedAt,
|
this.updatedAt,
|
||||||
this.role,
|
this.role,
|
||||||
|
this.bannedReason,
|
||||||
|
this.bannedUntil,
|
||||||
|
this.isBanned = false,
|
||||||
|
this.panicStrike = 0,
|
||||||
|
this.spoofingAttempts = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create an OfficerModel instance from a JSON object
|
// Create an OfficerModel instance from a JSON object
|
||||||
factory OfficerModel.fromJson(Map<String, dynamic> json) {
|
factory OfficerModel.fromJson(Map<String, dynamic> json) {
|
||||||
return OfficerModel(
|
return OfficerModel(
|
||||||
id: json['id'] as String,
|
id: json['id'],
|
||||||
unitId: json['unit_id'] as String,
|
unitId: json['unit_id'],
|
||||||
roleId: json['role_id'] as String,
|
roleId: json['role_id'],
|
||||||
patrolUnitId: json['patrol_unit_id'] as String?,
|
patrolUnitId: json['patrol_unit_id'],
|
||||||
nrp: json['nrp'] as String,
|
nrp: json['nrp'], // Accept as dynamic
|
||||||
name: json['name'] as String,
|
name: json['name'],
|
||||||
rank: json['rank'] as String?,
|
rank: json['rank'],
|
||||||
position: json['position'] as String?,
|
position: json['position'],
|
||||||
phone: json['phone'] as String?,
|
phone: json['phone'],
|
||||||
email: json['email'] as String?,
|
email: json['email'],
|
||||||
avatar: json['avatar'] as String?,
|
avatar: json['avatar'],
|
||||||
placeOfBirth: json['birth_place'] as String?,
|
placeOfBirth: json['place_of_birth'] ?? json['birth_place'],
|
||||||
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)
|
||||||
|
@ -64,7 +75,7 @@ class OfficerModel {
|
||||||
json['valid_until'] != null
|
json['valid_until'] != null
|
||||||
? DateTime.parse(json['valid_until'] as String)
|
? DateTime.parse(json['valid_until'] as String)
|
||||||
: null,
|
: null,
|
||||||
qrCode: json['qr_code'] as String?,
|
qrCode: json['qr_code'],
|
||||||
createdAt:
|
createdAt:
|
||||||
json['created_at'] != null
|
json['created_at'] != null
|
||||||
? DateTime.parse(json['created_at'] as String)
|
? DateTime.parse(json['created_at'] as String)
|
||||||
|
@ -73,10 +84,15 @@ class OfficerModel {
|
||||||
json['updated_at'] != null
|
json['updated_at'] != null
|
||||||
? DateTime.parse(json['updated_at'] as String)
|
? DateTime.parse(json['updated_at'] as String)
|
||||||
: null,
|
: null,
|
||||||
role:
|
// New fields
|
||||||
json['roles'] != null
|
bannedReason: json['banned_reason'],
|
||||||
? RoleModel.fromJson(json['roles'] as Map<String, dynamic>)
|
bannedUntil:
|
||||||
|
json['banned_until'] != null
|
||||||
|
? DateTime.parse(json['banned_until'] as String)
|
||||||
: null,
|
: null,
|
||||||
|
isBanned: json['is_banned'] ?? false,
|
||||||
|
panicStrike: json['panic_strike'] ?? 0,
|
||||||
|
spoofingAttempts: json['spoofing_attempts'] ?? 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,23 +110,60 @@ class OfficerModel {
|
||||||
'phone': phone,
|
'phone': phone,
|
||||||
'email': email,
|
'email': email,
|
||||||
'avatar': avatar,
|
'avatar': avatar,
|
||||||
'birth_place': placeOfBirth,
|
'place_of_birth': 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,
|
||||||
'created_at': createdAt?.toIso8601String(),
|
'created_at': createdAt?.toIso8601String(),
|
||||||
'updated_at': updatedAt?.toIso8601String(),
|
'updated_at': updatedAt?.toIso8601String(),
|
||||||
if (role != null) 'roles': role!.toJson(),
|
'banned_reason': bannedReason,
|
||||||
|
'banned_until': bannedUntil?.toIso8601String(),
|
||||||
|
'is_banned': isBanned,
|
||||||
|
'panic_strike': panicStrike,
|
||||||
|
'spoofing_attempts': spoofingAttempts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create to json non null fields
|
||||||
|
Map<String, dynamic> toJsonNonNull() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
|
||||||
|
if (id != null) json['id'] = id;
|
||||||
|
if (unitId != null) json['unit_id'] = unitId;
|
||||||
|
if (roleId != null) json['role_id'] = roleId;
|
||||||
|
if (patrolUnitId != null) json['patrol_unit_id'] = patrolUnitId;
|
||||||
|
if (nrp != null) json['nrp'] = nrp;
|
||||||
|
if (name != null) json['name'] = name;
|
||||||
|
if (rank != null) json['rank'] = rank;
|
||||||
|
if (position != null) json['position'] = position;
|
||||||
|
if (phone != null) json['phone'] = phone;
|
||||||
|
if (email != null) json['email'] = email;
|
||||||
|
if (avatar != null) json['avatar'] = avatar;
|
||||||
|
if (placeOfBirth != null) json['place_of_birth'] = placeOfBirth;
|
||||||
|
if (dateOfBirth != null)
|
||||||
|
json['date_of_birth'] = dateOfBirth!.toIso8601String();
|
||||||
|
if (validUntil != null) json['valid_until'] = validUntil!.toIso8601String();
|
||||||
|
if (qrCode != null) json['qr_code'] = qrCode;
|
||||||
|
if (createdAt != null) json['created_at'] = createdAt!.toIso8601String();
|
||||||
|
if (updatedAt != null) json['updated_at'] = updatedAt!.toIso8601String();
|
||||||
|
// New fields
|
||||||
|
if (bannedReason != null) json['banned_reason'] = bannedReason;
|
||||||
|
if (bannedUntil != null)
|
||||||
|
json['banned_until'] = bannedUntil!.toIso8601String();
|
||||||
|
json['is_banned'] = isBanned;
|
||||||
|
json['panic_strike'] = panicStrike;
|
||||||
|
json['spoofing_attempts'] = spoofingAttempts;
|
||||||
|
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
// Create a copy of the OfficerModel with updated fields
|
// Create a copy of the OfficerModel with updated fields
|
||||||
OfficerModel copyWith({
|
OfficerModel copyWith({
|
||||||
String? id,
|
String? id,
|
||||||
String? unitId,
|
String? unitId,
|
||||||
String? roleId,
|
String? roleId,
|
||||||
String? patrolUnitId,
|
String? patrolUnitId,
|
||||||
String? nrp,
|
dynamic nrp, // Changed to dynamic
|
||||||
String? name,
|
String? name,
|
||||||
String? rank,
|
String? rank,
|
||||||
String? position,
|
String? position,
|
||||||
|
@ -124,6 +177,11 @@ class OfficerModel {
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
DateTime? updatedAt,
|
DateTime? updatedAt,
|
||||||
RoleModel? role,
|
RoleModel? role,
|
||||||
|
String? bannedReason,
|
||||||
|
DateTime? bannedUntil,
|
||||||
|
bool? isBanned,
|
||||||
|
int? panicStrike,
|
||||||
|
int? spoofingAttempts,
|
||||||
}) {
|
}) {
|
||||||
return OfficerModel(
|
return OfficerModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
@ -144,6 +202,40 @@ class OfficerModel {
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
updatedAt: updatedAt ?? this.updatedAt,
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
role: role ?? this.role,
|
role: role ?? this.role,
|
||||||
|
bannedReason: bannedReason ?? this.bannedReason,
|
||||||
|
bannedUntil: bannedUntil ?? this.bannedUntil,
|
||||||
|
isBanned: isBanned ?? this.isBanned,
|
||||||
|
panicStrike: panicStrike ?? this.panicStrike,
|
||||||
|
spoofingAttempts: spoofingAttempts ?? this.spoofingAttempts,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an empty OfficerModel
|
||||||
|
factory OfficerModel.empty() {
|
||||||
|
return OfficerModel(
|
||||||
|
id: '',
|
||||||
|
unitId: '',
|
||||||
|
roleId: '',
|
||||||
|
patrolUnitId: null,
|
||||||
|
nrp: null,
|
||||||
|
name: '',
|
||||||
|
rank: null,
|
||||||
|
position: null,
|
||||||
|
phone: null,
|
||||||
|
email: null,
|
||||||
|
avatar: null,
|
||||||
|
placeOfBirth: null,
|
||||||
|
dateOfBirth: null,
|
||||||
|
validUntil: null,
|
||||||
|
qrCode: null,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
role: null,
|
||||||
|
bannedReason: null,
|
||||||
|
bannedUntil: null,
|
||||||
|
isBanned: false,
|
||||||
|
panicStrike: 0,
|
||||||
|
spoofingAttempts: 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,8 +270,6 @@ class OfficerModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'OfficerModel(id: $id, name: $name, nrp: $nrp)';
|
return 'OfficerModel(id: $id, name: $name, nrp: $nrp)';
|
||||||
|
|
|
@ -51,6 +51,18 @@ class RoleSelectionController extends GetxController {
|
||||||
role.name.toLowerCase() == 'officer',
|
role.name.toLowerCase() == 'officer',
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
// Set default role to Viewer
|
||||||
|
if (roles.isNotEmpty) {
|
||||||
|
// Find the viewer role
|
||||||
|
final viewerRole = roles.firstWhere(
|
||||||
|
(role) => role.name.toLowerCase() == 'viewer',
|
||||||
|
orElse: () => roles[0], // Fallback to first role if viewer not found
|
||||||
|
);
|
||||||
|
|
||||||
|
// Select the viewer role by default
|
||||||
|
selectRole(viewerRole);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
TLoaders.errorSnackBar(
|
TLoaders.errorSnackBar(
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
|
|
|
@ -138,7 +138,7 @@ class RoleSelectionScreen extends StatelessWidget {
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: SvgPicture.asset(
|
child: SvgPicture.asset(
|
||||||
TImages.homeOffice,
|
isDark ? TImages.communicationDark : TImages.communication,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: double.infinity,
|
height: double.infinity,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
|
|
|
@ -175,7 +175,12 @@ class UserMetadataModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create copy with updated fields
|
/// Create a copy of this model with modified attributes
|
||||||
|
///
|
||||||
|
/// This method allows for creating a new instance with selectively updated fields.
|
||||||
|
/// For fields not explicitly provided, the original values are retained.
|
||||||
|
///
|
||||||
|
/// Setting 'isOfficer' will keep consistency with officer/viewer data.
|
||||||
UserMetadataModel copyWith({
|
UserMetadataModel copyWith({
|
||||||
bool? isOfficer,
|
bool? isOfficer,
|
||||||
String? userId,
|
String? userId,
|
||||||
|
@ -186,22 +191,49 @@ class UserMetadataModel {
|
||||||
UserModel? viewerData,
|
UserModel? viewerData,
|
||||||
Map<String, dynamic>? additionalData,
|
Map<String, dynamic>? additionalData,
|
||||||
}) {
|
}) {
|
||||||
|
final newIsOfficer = isOfficer ?? this.isOfficer;
|
||||||
|
|
||||||
return UserMetadataModel(
|
return UserMetadataModel(
|
||||||
isOfficer: isOfficer ?? this.isOfficer,
|
isOfficer: newIsOfficer,
|
||||||
userId: userId ?? this.userId,
|
userId: userId ?? this.userId,
|
||||||
roleId: roleId ?? this.roleId,
|
roleId: roleId ?? this.roleId,
|
||||||
profileStatus: profileStatus ?? this.profileStatus,
|
profileStatus: profileStatus ?? this.profileStatus,
|
||||||
email: email ?? this.email,
|
email: email ?? this.email,
|
||||||
officerData: officerData ?? this.officerData,
|
// Only include officer data if the model is for an officer
|
||||||
viewerData: viewerData ?? this.viewerData,
|
officerData: newIsOfficer ? (officerData ?? this.officerData) : null,
|
||||||
|
// Only include viewer data if the model is not for an officer
|
||||||
|
viewerData: !newIsOfficer ? (viewerData ?? this.viewerData) : null,
|
||||||
additionalData: additionalData ?? this.additionalData,
|
additionalData: additionalData ?? this.additionalData,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new UserMetadataModel with profile status set to 'completed'
|
||||||
|
UserMetadataModel markAsCompleted() {
|
||||||
|
return copyWith(profileStatus: 'completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new UserMetadataModel with updated officer data
|
||||||
|
UserMetadataModel withUpdatedOfficerData(OfficerModel officer) {
|
||||||
|
return copyWith(
|
||||||
|
isOfficer: true,
|
||||||
|
officerData: officer,
|
||||||
|
profileStatus: 'completed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new UserMetadataModel with updated viewer data
|
||||||
|
UserMetadataModel withUpdatedViewerData(UserModel viewer) {
|
||||||
|
return copyWith(
|
||||||
|
isOfficer: false,
|
||||||
|
viewerData: viewer,
|
||||||
|
profileStatus: 'completed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Computed properties (getters)
|
// MARK: - Computed properties (getters)
|
||||||
|
|
||||||
/// Primary identifier (NRP for officers, NIK for users)
|
/// Primary identifier (NRP for officers, NIK for users)
|
||||||
String? get identifier => isOfficer ? officerData?.nrp : nik;
|
String? get identifier => isOfficer ? officerData?.nrp?.toString() : nik;
|
||||||
|
|
||||||
/// User's NIK (delegated to viewerData if available)
|
/// User's NIK (delegated to viewerData if available)
|
||||||
String? get nik => viewerData?.profile?.nik;
|
String? get nik => viewerData?.profile?.nik;
|
||||||
|
@ -211,7 +243,7 @@ class UserMetadataModel {
|
||||||
|
|
||||||
/// User's name (delegated to appropriate model or fallback to email)
|
/// User's name (delegated to appropriate model or fallback to email)
|
||||||
String? get name {
|
String? get name {
|
||||||
if (isOfficer && officerData?.name.isNotEmpty == true) {
|
if (isOfficer && officerData?.name?.isNotEmpty == true) {
|
||||||
return officerData!.name;
|
return officerData!.name;
|
||||||
}
|
}
|
||||||
if (!isOfficer && viewerData?.profile?.fullName?.isNotEmpty == true) {
|
if (!isOfficer && viewerData?.profile?.fullName?.isNotEmpty == true) {
|
||||||
|
@ -238,10 +270,10 @@ class UserMetadataModel {
|
||||||
final errors = <String>[];
|
final errors = <String>[];
|
||||||
|
|
||||||
if (isOfficer) {
|
if (isOfficer) {
|
||||||
if (officerData?.nrp.isEmpty != false) {
|
if (officerData?.nrp == null) {
|
||||||
errors.add('NRP is required for officers');
|
errors.add('NRP is required for officers');
|
||||||
}
|
}
|
||||||
if (officerData?.unitId.isEmpty != false) {
|
if (officerData?.unitId?.isEmpty != false) {
|
||||||
errors.add('Unit ID is required for officers');
|
errors.add('Unit ID is required for officers');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart';
|
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||||
|
@ -64,6 +65,8 @@ class OfficerRepository extends GetxController {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger().i('Officer updated successfully: $updatedOfficer');
|
||||||
|
|
||||||
// updatedOfficer is a List, so we take the first item and convert it
|
// updatedOfficer is a List, so we take the first item and convert it
|
||||||
return OfficerModel.fromJson(updatedOfficer);
|
return OfficerModel.fromJson(updatedOfficer);
|
||||||
} on PostgrestException catch (error) {
|
} on PostgrestException catch (error) {
|
||||||
|
@ -91,6 +94,8 @@ class OfficerRepository extends GetxController {
|
||||||
.eq('id', officerId)
|
.eq('id', officerId)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
Logger().i('Fetched officer data: $officerData');
|
||||||
|
|
||||||
return OfficerModel.fromJson(officerData);
|
return OfficerModel.fromJson(officerData);
|
||||||
} on PostgrestException catch (error) {
|
} on PostgrestException catch (error) {
|
||||||
throw TExceptions.fromCode(error.code!);
|
throw TExceptions.fromCode(error.code!);
|
||||||
|
|
|
@ -116,8 +116,13 @@ class UserRepository extends GetxController {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
throw 'User not authenticated';
|
throw 'User not authenticated';
|
||||||
}
|
}
|
||||||
|
Logger().i('Updating user metadata: $metadata');
|
||||||
|
|
||||||
await _supabase.auth.updateUser(UserAttributes(data: metadata));
|
final updatedMetadata = await _supabase.auth.updateUser(
|
||||||
|
UserAttributes(data: metadata),
|
||||||
|
);
|
||||||
|
|
||||||
|
Logger().i('User metadata updated successfully: $updatedMetadata');
|
||||||
} on AuthException catch (e) {
|
} 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);
|
||||||
|
@ -127,6 +132,27 @@ class UserRepository extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// update profile status in user metadata
|
||||||
|
Future<void> updateProfileStatus(String status) async {
|
||||||
|
try {
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
throw 'User not authenticated';
|
||||||
|
}
|
||||||
|
|
||||||
|
final metadata = {
|
||||||
|
'profile_status': status,
|
||||||
|
};
|
||||||
|
|
||||||
|
await updateUserMetadata(metadata);
|
||||||
|
} on AuthException catch (e) {
|
||||||
|
_logger.e('AuthException in updateProfileStatus: ${e.message}');
|
||||||
|
throw TExceptions(e.message);
|
||||||
|
} catch (e) {
|
||||||
|
_logger.e('Exception in updateProfileStatus: $e');
|
||||||
|
throw 'Failed to update profile status: ${e.toString()}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update user email
|
// Update user email
|
||||||
Future<void> updateUserEmail(String newEmail) async {
|
Future<void> updateUserEmail(String newEmail) async {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:ui'; // Add this import for ImageFilter
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
@ -379,28 +380,51 @@ class ImageUploader extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _defaultErrorOverlay() {
|
Widget _defaultErrorOverlay() {
|
||||||
return Container(
|
return ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||||
|
child: Container(
|
||||||
height: 200,
|
height: 200,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2),
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2),
|
||||||
color: TColors.error.withOpacity(0.2),
|
color: Colors.black.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
TColors.error.withOpacity(0.4),
|
||||||
|
Colors.black.withOpacity(0.7),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: TColors.error.withOpacity(0.2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
Icons.error_outline,
|
Icons.error_outline,
|
||||||
color: TColors.error,
|
color: Colors.white,
|
||||||
size: TSizes.iconLg,
|
size: TSizes.iconLg,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: TSizes.sm),
|
const SizedBox(height: TSizes.sm),
|
||||||
Text(
|
Text(
|
||||||
'Invalid Image',
|
'Invalid Image',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: TColors.error,
|
color: Colors.white,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.xs),
|
const SizedBox(height: TSizes.xs),
|
||||||
|
@ -410,7 +434,7 @@ class ImageUploader extends StatelessWidget {
|
||||||
errorMessage ?? 'Please try another image',
|
errorMessage ?? 'Please try another image',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: TColors.error,
|
color: Colors.white.withOpacity(0.9),
|
||||||
fontSize: TSizes.fontSizeSm,
|
fontSize: TSizes.fontSizeSm,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -418,6 +442,9 @@ class ImageUploader extends StatelessWidget {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -447,4 +474,5 @@ class ImageUploader extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
|
||||||
|
class StepNavigationButtons extends StatelessWidget {
|
||||||
|
final bool showPrevious;
|
||||||
|
final bool isLastStep;
|
||||||
|
final VoidCallback onPrevious;
|
||||||
|
final VoidCallback onNext;
|
||||||
|
final bool isLoading;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String nextButtonText;
|
||||||
|
final String previousButtonText;
|
||||||
|
final bool
|
||||||
|
useDefaultNavigation; // New parameter for custom navigation control
|
||||||
|
|
||||||
|
const StepNavigationButtons({
|
||||||
|
super.key,
|
||||||
|
this.showPrevious = true,
|
||||||
|
this.isLastStep = false,
|
||||||
|
required this.onPrevious,
|
||||||
|
required this.onNext,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.errorMessage,
|
||||||
|
this.nextButtonText = 'Next',
|
||||||
|
this.previousButtonText = 'Previous',
|
||||||
|
this.useDefaultNavigation =
|
||||||
|
true, // Default to true for backward compatibility
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Error message if any
|
||||||
|
if (errorMessage != null && errorMessage!.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
margin: const EdgeInsets.only(bottom: TSizes.sm),
|
||||||
|
padding: const EdgeInsets.all(TSizes.sm),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: TColors.error.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusSm),
|
||||||
|
border: Border.all(color: TColors.error),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, color: TColors.error, size: 20),
|
||||||
|
const SizedBox(width: TSizes.sm),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
errorMessage!,
|
||||||
|
style: TextStyle(color: TColors.error, fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Navigation buttons
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Back button
|
||||||
|
if (showPrevious)
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: TSizes.sm),
|
||||||
|
child: ElevatedButton(
|
||||||
|
// Use onPrevious only if not loading and either useDefaultNavigation is true or we're handling custom navigation
|
||||||
|
onPressed: isLoading ? null : onPrevious,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[300],
|
||||||
|
foregroundColor: Colors.black87,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
disabledBackgroundColor: Colors.grey[200],
|
||||||
|
disabledForegroundColor: Colors.grey,
|
||||||
|
),
|
||||||
|
child: Text(previousButtonText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Next/Submit button
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
// For next button, respect the useDefaultNavigation flag
|
||||||
|
onPressed:
|
||||||
|
isLoading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
// If using default navigation or validation logic, just call the provided callback
|
||||||
|
if (useDefaultNavigation) {
|
||||||
|
onNext();
|
||||||
|
} else {
|
||||||
|
// For custom navigation, the caller is responsible for form validation
|
||||||
|
// and navigation logic in the onNext callback
|
||||||
|
onNext();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: TColors.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
disabledBackgroundColor: TColors.primary.withOpacity(0.3),
|
||||||
|
),
|
||||||
|
child:
|
||||||
|
isLoading
|
||||||
|
? const Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: TSizes.xs),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Text(isLastStep ? 'Submit' : nextButtonText),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:sigap/src/shared/styles/spacing_styles.dart';
|
import 'package:sigap/src/shared/styles/spacing_styles.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
@ -20,6 +21,7 @@ class StateScreen extends StatelessWidget {
|
||||||
this.primaryButtonTitle = 'Continue',
|
this.primaryButtonTitle = 'Continue',
|
||||||
this.onSecondaryPressed,
|
this.onSecondaryPressed,
|
||||||
this.isLottie = false,
|
this.isLottie = false,
|
||||||
|
this.isSvg = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String? image;
|
final String? image;
|
||||||
|
@ -28,6 +30,7 @@ class StateScreen extends StatelessWidget {
|
||||||
final String primaryButtonTitle;
|
final String primaryButtonTitle;
|
||||||
final String secondaryTitle;
|
final String secondaryTitle;
|
||||||
final bool? isLottie;
|
final bool? isLottie;
|
||||||
|
final bool isSvg;
|
||||||
final VoidCallback? onPressed;
|
final VoidCallback? onPressed;
|
||||||
final VoidCallback? onSecondaryPressed;
|
final VoidCallback? onSecondaryPressed;
|
||||||
final bool showButton;
|
final bool showButton;
|
||||||
|
@ -50,7 +53,7 @@ class StateScreen extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Image, Icon, or Lottie
|
// Image, Icon, Lottie, or SVG
|
||||||
if (icon != null)
|
if (icon != null)
|
||||||
Icon(
|
Icon(
|
||||||
icon,
|
icon,
|
||||||
|
@ -62,6 +65,15 @@ class StateScreen extends StatelessWidget {
|
||||||
image!,
|
image!,
|
||||||
width: THelperFunctions.screenWidth() * 0.8,
|
width: THelperFunctions.screenWidth() * 0.8,
|
||||||
)
|
)
|
||||||
|
else if (isSvg && image != null)
|
||||||
|
SvgPicture.asset(
|
||||||
|
image!,
|
||||||
|
width: THelperFunctions.screenWidth() * 0.8,
|
||||||
|
colorFilter: const ColorFilter.mode(
|
||||||
|
TColors.primary,
|
||||||
|
BlendMode.srcIn,
|
||||||
|
),
|
||||||
|
)
|
||||||
else if (image != null)
|
else if (image != null)
|
||||||
Image(
|
Image(
|
||||||
image: AssetImage(image!),
|
image: AssetImage(image!),
|
||||||
|
|
|
@ -20,6 +20,7 @@ class CustomTextField extends StatelessWidget {
|
||||||
final void Function(String)? onChanged;
|
final void Function(String)? onChanged;
|
||||||
final Color? accentColor;
|
final Color? accentColor;
|
||||||
final Color? fillColor;
|
final Color? fillColor;
|
||||||
|
final InputDecoration? decoration; // New parameter for custom decoration
|
||||||
|
|
||||||
const CustomTextField({
|
const CustomTextField({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -40,8 +41,8 @@ class CustomTextField extends StatelessWidget {
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
this.accentColor,
|
this.accentColor,
|
||||||
this.fillColor,
|
this.fillColor,
|
||||||
|
this.decoration, // Add to constructor
|
||||||
}) : assert(
|
}) : assert(
|
||||||
// Fix the assertion to avoid duplicate conditions
|
|
||||||
controller == null || initialValue == null,
|
controller == null || initialValue == null,
|
||||||
'Either provide a controller or an initialValue, not both',
|
'Either provide a controller or an initialValue, not both',
|
||||||
);
|
);
|
||||||
|
@ -59,8 +60,10 @@ class CustomTextField extends StatelessWidget {
|
||||||
? (isDark ? Colors.grey[800]! : Colors.grey[200]!)
|
? (isDark ? Colors.grey[800]! : Colors.grey[200]!)
|
||||||
: fillColor ?? (isDark ? TColors.dark : TColors.lightContainer);
|
: fillColor ?? (isDark ? TColors.dark : TColors.lightContainer);
|
||||||
|
|
||||||
// Get the common input decoration for both cases
|
// Get the input decoration - either custom or default
|
||||||
final inputDecoration = _getInputDecoration(
|
final inputDecoration =
|
||||||
|
decoration ??
|
||||||
|
_getInputDecoration(
|
||||||
context,
|
context,
|
||||||
effectiveAccentColor,
|
effectiveAccentColor,
|
||||||
isDark,
|
isDark,
|
||||||
|
|
|
@ -2,5 +2,6 @@ class TNum {
|
||||||
// Auth Number
|
// Auth Number
|
||||||
static const int oneTimePassword = 6;
|
static const int oneTimePassword = 6;
|
||||||
static const int totalStepViewer = 4;
|
static const int totalStepViewer = 4;
|
||||||
static const int totalStepOfficer = 4;
|
static const int totalStepOfficer =
|
||||||
|
3; // Reduced from 4 to 3 steps for officers
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,12 +326,12 @@ model patrol_units {
|
||||||
location_id String @db.Uuid
|
location_id String @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
type String @db.VarChar(50)
|
type String @db.VarChar(50)
|
||||||
category patrol_unit_category? @default(group)
|
|
||||||
member_count Int? @default(0)
|
|
||||||
status String @db.VarChar(50)
|
status String @db.VarChar(50)
|
||||||
radius Float
|
radius Float
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||||
id String @id @unique @db.VarChar(100)
|
id String @id @unique @db.VarChar(100)
|
||||||
|
category patrol_unit_category? @default(group)
|
||||||
|
member_count Int? @default(0)
|
||||||
members officers[]
|
members officers[]
|
||||||
location locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
location locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
unit units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
unit units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
Loading…
Reference in New Issue