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:
|
currentStep.value++;
|
||||||
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++;
|
|
||||||
} 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,
|
||||||
|
);
|
||||||
|
|
||||||
// final updatedOfficer = await OfficerRepository.instance.updateOfficer(
|
// Logger().i('Updating officer with data: ${updateOfficer.toJson()}');
|
||||||
// data,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// if (updatedOfficer == null) {
|
final updatedOfficer = await OfficerRepository.instance.updateOfficer(
|
||||||
// TLoaders.errorSnackBar(
|
updateOfficer,
|
||||||
// title: 'Update Failed',
|
);
|
||||||
// message: 'Failed to update officer information. Please try again.',
|
|
||||||
// );
|
|
||||||
// TCircularFullScreenLoader.stopLoading();
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// final userMetadata =
|
if (updatedOfficer == null) {
|
||||||
// metadata.copyWith(profileStatus: 'completed').toAuthMetadataJson();
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Update Failed',
|
||||||
|
message: 'Failed to update officer information. Please try again.',
|
||||||
|
);
|
||||||
|
TCircularFullScreenLoader.stopLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// await UserRepository.instance.updateUserMetadata(userMetadata);
|
await UserRepository.instance.updateProfileStatus('completed');
|
||||||
|
|
||||||
// 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,66 +104,36 @@ 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;
|
||||||
|
|
||||||
switch (controller.currentStep.value) {
|
// Different step content for officer vs viewer
|
||||||
case 0:
|
if (isOfficer) {
|
||||||
return const PersonalInfoStep();
|
// Officer registration flow (3 steps)
|
||||||
case 1:
|
switch (controller.currentStep.value) {
|
||||||
return const IdCardVerificationStep();
|
case 0:
|
||||||
case 2:
|
return const IdCardVerificationStep();
|
||||||
return const SelfieVerificationStep();
|
case 1:
|
||||||
case 3:
|
return const SelfieVerificationStep();
|
||||||
return isOfficer
|
case 2:
|
||||||
? const OfficerInfoStep()
|
return const OfficerInfoStep();
|
||||||
: const IdentityVerificationStep();
|
default:
|
||||||
default:
|
return const SizedBox.shrink();
|
||||||
return const SizedBox.shrink();
|
}
|
||||||
|
} else {
|
||||||
|
// Viewer registration flow (4 steps)
|
||||||
|
switch (controller.currentStep.value) {
|
||||||
|
case 0:
|
||||||
|
return const PersonalInfoStep();
|
||||||
|
case 1:
|
||||||
|
return const IdCardVerificationStep();
|
||||||
|
case 2:
|
||||||
|
return const SelfieVerificationStep();
|
||||||
|
case 3:
|
||||||
|
return const IdentityVerificationStep();
|
||||||
|
default:
|
||||||
|
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,
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
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';
|
||||||
|
@ -54,7 +55,7 @@ class OfficerRepository extends GetxController {
|
||||||
|
|
||||||
final updatedOfficer =
|
final updatedOfficer =
|
||||||
await _supabase
|
await _supabase
|
||||||
.from('officers')
|
.from('officers')
|
||||||
.update(data)
|
.update(data)
|
||||||
.eq('id', currentUserId!)
|
.eq('id', currentUserId!)
|
||||||
.select()
|
.select()
|
||||||
|
@ -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,43 +380,69 @@ class ImageUploader extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _defaultErrorOverlay() {
|
Widget _defaultErrorOverlay() {
|
||||||
return Container(
|
return ClipRRect(
|
||||||
height: 200,
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2),
|
||||||
width: double.infinity,
|
child: BackdropFilter(
|
||||||
decoration: BoxDecoration(
|
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2),
|
child: Container(
|
||||||
color: TColors.error.withOpacity(0.2),
|
height: 200,
|
||||||
),
|
width: double.infinity,
|
||||||
child: Center(
|
decoration: BoxDecoration(
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd - 2),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
color: Colors.black.withOpacity(0.5),
|
||||||
children: [
|
),
|
||||||
Icon(
|
child: Container(
|
||||||
Icons.error_outline,
|
decoration: BoxDecoration(
|
||||||
color: TColors.error,
|
gradient: LinearGradient(
|
||||||
size: TSizes.iconLg,
|
begin: Alignment.topCenter,
|
||||||
),
|
end: Alignment.bottomCenter,
|
||||||
const SizedBox(height: TSizes.sm),
|
colors: [
|
||||||
Text(
|
TColors.error.withOpacity(0.4),
|
||||||
'Invalid Image',
|
Colors.black.withOpacity(0.7),
|
||||||
style: TextStyle(
|
],
|
||||||
color: TColors.error,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.xs),
|
child: Center(
|
||||||
Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: TSizes.md),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Text(
|
children: [
|
||||||
errorMessage ?? 'Please try another image',
|
Container(
|
||||||
textAlign: TextAlign.center,
|
padding: const EdgeInsets.all(10),
|
||||||
style: TextStyle(
|
decoration: BoxDecoration(
|
||||||
color: TColors.error,
|
color: TColors.error.withOpacity(0.2),
|
||||||
fontSize: TSizes.fontSizeSm,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: Colors.white,
|
||||||
|
size: TSizes.iconLg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: TSizes.sm),
|
||||||
|
Text(
|
||||||
|
'Invalid Image',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: TSizes.xs),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: TSizes.md),
|
||||||
|
child: Text(
|
||||||
|
errorMessage ?? 'Please try another image',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withOpacity(0.9),
|
||||||
|
fontSize: TSizes.fontSizeSm,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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