feat: Enhance Selfie Verification Step with Dark Mode Support and Improved Snackbars

- Added dark mode support for warning colors in the Selfie Verification Step.
- Replaced hardcoded colors with theme-aware colors for better UI consistency.
- Updated Snackbar notifications to use a centralized loader utility for better user feedback.
- Refactored the auto-verify toggle and bypass liveness check functionality for clarity.
- Improved layout and styling for better user experience.

fix: Update Units Repository with Logging and Error Handling

- Integrated logging for unit fetching operations in the Units Repository.
- Added checks for empty unit lists to prevent unnecessary errors.
- Improved error handling for database operations.

refactor: Revamp Image Source Dialog and Uploader for Dark Mode

- Enhanced Image Source Dialog to support dark mode styling.
- Updated Image Uploader to use a custom circular loader for better visual feedback during uploads.

style: Standardize Step Indicator Colors Based on Theme

- Adjusted step indicator icon colors to adapt based on the current theme (dark/light).

chore: Update Tips Container to Use New Color Constants

- Refactored Tips Container to utilize new color constants for better maintainability.

style: Introduce New Color Constants for Card Styles

- Added new color constants for card backgrounds, borders, and text to improve UI consistency across the application.

fix: Adjust Total Steps for Officer Selection Process

- Updated the total steps for officer selection from 5 to 4 to reflect the current flow.

chore: Add Patrol Unit Selection Screen for Officer Information

- Implemented a new Patrol Unit Selection Screen to allow officers to configure their patrol units.
- Integrated selection modes and unit creation functionality for enhanced user experience.
This commit is contained in:
vergiLgood1 2025-05-26 13:31:12 +07:00
parent 423c5a369f
commit 5bd92a0399
17 changed files with 1859 additions and 333 deletions

View File

@ -195,13 +195,6 @@ class FormRegistrationController extends GetxController {
if (registrationData.value.roleId?.isNotEmpty == true) { if (registrationData.value.roleId?.isNotEmpty == true) {
await _setRoleFromMetadata(); await _setRoleFromMetadata();
} }
if (registrationData.value.isOfficer ||
(selectedRole.value?.isOfficer == true)) {
await _fetchAvailableUnits();
}
Logger().d('Initialization completed successfully');
} catch (e) { } catch (e) {
Logger().e('Error completing initialization: $e'); Logger().e('Error completing initialization: $e');
} }
@ -227,7 +220,10 @@ class FormRegistrationController extends GetxController {
} }
void _initializeControllers() { void _initializeControllers() {
final isOfficer = registrationData.value.isOfficer; final isOfficer =
AuthenticationRepository.instance.authUser?.userMetadata?['is_officer']
as bool? ??
false;
Logger().d('Initializing controllers with isOfficer: $isOfficer'); Logger().d('Initializing controllers with isOfficer: $isOfficer');
@ -296,8 +292,10 @@ class FormRegistrationController extends GetxController {
permanent: false, permanent: false,
); );
Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false); if (isOfficer) {
Get.put<UnitInfoController>(UnitInfoController(), permanent: false); Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false);
Get.put<UnitInfoController>(UnitInfoController(), permanent: false);
}
} }
void _assignControllerReferences(bool isOfficer) { void _assignControllerReferences(bool isOfficer) {
@ -626,24 +624,6 @@ class FormRegistrationController extends GetxController {
} }
} }
Future<void> _fetchAvailableUnits() async {
try {
isLoading.value = true;
await Future.delayed(const Duration(seconds: 1));
if (unitInfoController != null) {
// unitInfoController!.availableUnits.value = fetchedUnits;
}
} catch (e) {
TLoaders.errorSnackBar(
title: 'Error',
message: 'Failed to fetch available units: ${e.toString()}',
);
} finally {
isLoading.value = false;
}
}
// Validate current step // Validate current step
bool validateCurrentStep() { bool validateCurrentStep() {
switch (currentStep.value) { switch (currentStep.value) {
@ -737,7 +717,7 @@ class FormRegistrationController extends GetxController {
break; break;
case 3: case 3:
if (selectedRole.value?.isOfficer == true) { if (selectedRole.value?.isOfficer == true) {
officerInfoController!.clearErrors(); officerInfoController?.clearErrors();
} else { } else {
identityController.clearErrors(); identityController.clearErrors();
} }

View File

@ -1,7 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:sigap/src/features/daily-ops/data/models/models/patrol_units_model.dart';
import 'package:sigap/src/features/daily-ops/data/models/models/units_model.dart';
import 'package:sigap/src/features/daily-ops/data/repositories/patrol_units_repository.dart';
import 'package:sigap/src/features/daily-ops/data/repositories/units_repository.dart';
import 'package:sigap/src/utils/popups/loaders.dart';
import 'package:sigap/src/utils/validators/validation.dart'; import 'package:sigap/src/utils/validators/validation.dart';
// Define enums for patrol unit type and selection mode
enum PatrolUnitType { car, motorcycle }
enum PatrolSelectionMode { individual, group, createNew }
class OfficerInfoController extends GetxController { class OfficerInfoController extends GetxController {
// Singleton instance // Singleton instance
static OfficerInfoController get instance => Get.find(); static OfficerInfoController get instance => Get.find();
@ -9,7 +20,23 @@ class OfficerInfoController extends GetxController {
// Static form key // Static form key
// final GlobalKey<FormState> formKey = TGlobalFormKey.officerInfo(); // final GlobalKey<FormState> formKey = TGlobalFormKey.officerInfo();
final RxBool isFormValid = RxBool(true); final RxBool isFormValid = RxBool(true);
// Data states
final RxList<UnitModel> availableUnits = <UnitModel>[].obs;
final RxList<PatrolUnitModel> availablePatrolUnits = <PatrolUnitModel>[].obs;
final RxBool isLoadingUnits = false.obs;
final RxBool isLoadingPatrolUnits = false.obs;
final RxString selectedUnitName = ''.obs;
final RxString selectedPatrolUnitName = ''.obs;
// Additional data states for patrol unit configuration
final Rx<PatrolUnitType> selectedPatrolType = PatrolUnitType.car.obs;
final Rx<PatrolSelectionMode> patrolSelectionMode =
PatrolSelectionMode.individual.obs;
final RxBool isCreatingNewPatrolUnit = false.obs;
final RxString newPatrolUnitName = ''.obs;
// Controllers // Controllers
final nrpController = TextEditingController(); final nrpController = TextEditingController();
final rankController = TextEditingController(); final rankController = TextEditingController();
@ -25,6 +52,13 @@ final RxBool isFormValid = RxBool(true);
final bannedReasonController = TextEditingController(); final bannedReasonController = TextEditingController();
final bannedUntilController = TextEditingController(); final bannedUntilController = TextEditingController();
// Controllers for new patrol unit
final patrolNameController = TextEditingController();
final patrolTypeController = TextEditingController();
final patrolRadiusController = TextEditingController(
text: '500',
); // Default radius in meters
// Error states // Error states
final RxString nrpError = ''.obs; final RxString nrpError = ''.obs;
final RxString rankError = ''.obs; final RxString rankError = ''.obs;
@ -40,6 +74,256 @@ final RxBool isFormValid = RxBool(true);
final RxString bannedReasonError = ''.obs; final RxString bannedReasonError = ''.obs;
final RxString bannedUntilError = ''.obs; final RxString bannedUntilError = ''.obs;
// Error states for new patrol unit
final RxString patrolNameError = ''.obs;
final RxString patrolTypeError = ''.obs;
final RxString patrolRadiusError = ''.obs;
// Logger instance
final Logger logger = Logger();
// Make sure repositories are properly initialized
late final UnitRepository unitRepository;
late final PatrolUnitRepository patrolUnitRepository;
// Dropdown open state
RxBool isUnitDropdownOpen = false.obs;
@override
void onInit() {
super.onInit();
initRepositories();
// Fetch units after ensuring repositories are set up
getAvailableUnits();
}
void initRepositories() {
// Check if repositories are already registered with GetX
unitRepository = Get.find<UnitRepository>();
patrolUnitRepository = Get.find<PatrolUnitRepository>();
Logger().i('UnitRepository and PatrolUnitRepository initialized');
}
// Fetch available units with improved error handling
void getAvailableUnits() async {
logger.i('Starting to fetch available units');
try {
isLoadingUnits.value = true;
availableUnits.clear();
logger.i('Calling repository.getAllUnits()');
final units = await unitRepository.getAllUnits();
logger.i('Received ${units.length} units from repository');
availableUnits.value = units;
if (units.isEmpty) {
logger.w('No units returned from repository');
} else {
logger.i('First unit: ${units.first.name}');
}
isLoadingUnits.value = false;
} catch (error) {
logger.e('Failed to fetch units: $error');
isLoadingUnits.value = false;
TLoaders.errorSnackBar(
title: 'Error',
message:
'Something went wrong while fetching units. Please try again later.',
);
}
}
// Fetch patrol units by unit ID
void getPatrolUnitsByUnitId(String unitId) async {
try {
isLoadingPatrolUnits.value = true;
availablePatrolUnits.clear();
final patrolUnits = await patrolUnitRepository.getPatrolUnitsByUnitId(
unitId,
);
availablePatrolUnits.value = patrolUnits;
isLoadingPatrolUnits.value = false;
} catch (error) {
isLoadingPatrolUnits.value = false;
TLoaders.errorSnackBar(
title: 'Error',
message:
'Something went wrong while fetching patrol units. Please try again later.',
);
Logger().e('Failed to fetch patrol units: $error');
}
}
// Handle unit selection
void onUnitSelected(UnitModel unit) {
unitIdController.text = unit.codeUnit;
selectedUnitName.value = unit.name;
// Clear patrol unit selection
patrolUnitIdController.text = '';
selectedPatrolUnitName.value = '';
// Get patrol units for the selected unit
getPatrolUnitsByUnitId(unit.codeUnit);
}
// Handle patrol unit selection
void onPatrolUnitSelected(PatrolUnitModel patrolUnit) {
patrolUnitIdController.text = patrolUnit.id;
selectedPatrolUnitName.value = patrolUnit.name;
}
// Set patrol unit type (car or motorcycle)
void setPatrolUnitType(PatrolUnitType type) {
selectedPatrolType.value = type;
patrolTypeController.text = type.name;
}
// Set patrol selection mode (individual, group, or create new)
void setPatrolSelectionMode(PatrolSelectionMode mode) {
patrolSelectionMode.value = mode;
// Reset fields when changing modes
if (mode == PatrolSelectionMode.createNew) {
isCreatingNewPatrolUnit.value = true;
patrolUnitIdController.clear();
selectedPatrolUnitName.value = '';
} else {
isCreatingNewPatrolUnit.value = false;
}
}
// Validate new patrol unit data
bool validateNewPatrolUnit() {
bool isValid = true;
// Clear previous errors
patrolNameError.value = '';
patrolTypeError.value = '';
patrolRadiusError.value = '';
// Validate patrol unit name
final nameValidation = TValidators.validateUserInput(
'Patrol Unit Name',
patrolNameController.text,
50,
);
if (nameValidation != null) {
patrolNameError.value = nameValidation;
isValid = false;
}
// Validate patrol unit type
if (patrolTypeController.text.isEmpty) {
patrolTypeError.value = 'Patrol type is required';
isValid = false;
}
// Validate patrol radius
if (patrolRadiusController.text.isEmpty) {
patrolRadiusError.value = 'Patrol radius is required';
isValid = false;
} else {
try {
final radius = double.parse(patrolRadiusController.text);
if (radius <= 0) {
patrolRadiusError.value = 'Radius must be greater than 0';
isValid = false;
}
} catch (e) {
patrolRadiusError.value = 'Invalid radius value';
isValid = false;
}
}
return isValid;
}
// Create a new patrol unit
Future<bool> createNewPatrolUnit() async {
if (!validateNewPatrolUnit()) {
return false;
}
if (unitIdController.text.isEmpty) {
TLoaders.errorSnackBar(
title: 'Unit Required',
message: 'Please select a unit before creating a patrol unit',
);
return false;
}
try {
// This would typically involve an API call to create the patrol unit
// For now, we'll just simulate success
// In a real implementation, you would call a repository method to create the patrol unit
// Example of what the real implementation might look like:
/*
final newPatrolUnit = await patrolUnitRepository.createPatrolUnit(
PatrolUnitModel(
id: '', // Will be generated by the backend
unitId: unitIdController.text,
locationId: '', // This might need to be set elsewhere
name: patrolNameController.text,
type: selectedPatrolType.value.name,
status: 'active', // Default status
radius: double.parse(patrolRadiusController.text),
createdAt: DateTime.now(),
),
);
// If successful, set the patrol unit ID and name
patrolUnitIdController.text = newPatrolUnit.id;
selectedPatrolUnitName.value = newPatrolUnit.name;
*/
// Simulate success
TLoaders.successSnackBar(
title: 'Success',
message: 'Patrol unit created successfully',
);
return true;
} catch (error) {
TLoaders.errorSnackBar(
title: 'Error',
message: 'Failed to create patrol unit: $error',
);
return false;
}
}
// Join an existing patrol unit
void joinPatrolUnit(PatrolUnitModel patrolUnit) {
patrolUnitIdController.text = patrolUnit.id;
selectedPatrolUnitName.value = patrolUnit.name;
// In a real app, you might want to update the user's patrol unit membership
// This could involve an API call to update the user's patrol unit ID
}
// Get available patrol units filtered by type
List<PatrolUnitModel> getFilteredPatrolUnits() {
if (selectedPatrolType.value == PatrolUnitType.car) {
return availablePatrolUnits
.where((unit) => unit.type.toLowerCase() == 'car')
.toList();
} else {
return availablePatrolUnits
.where((unit) => unit.type.toLowerCase() == 'motorcycle')
.toList();
}
}
bool validate(GlobalKey<FormState> formKey) { bool validate(GlobalKey<FormState> formKey) {
clearErrors(); clearErrors();
@ -47,8 +331,6 @@ final RxBool isFormValid = RxBool(true);
return true; return true;
} }
final nrpValidation = TValidators.validateUserInput( final nrpValidation = TValidators.validateUserInput(
'NRP', 'NRP',
nrpController.text, nrpController.text,
@ -179,6 +461,11 @@ final RxBool isFormValid = RxBool(true);
isFormValid.value = false; isFormValid.value = false;
} }
// Include validation for new patrol unit if creating one
if (isCreatingNewPatrolUnit.value && !validateNewPatrolUnit()) {
isFormValid.value = false;
}
return isFormValid.value; return isFormValid.value;
} }
@ -196,6 +483,11 @@ final RxBool isFormValid = RxBool(true);
qrCodeError.value = ''; qrCodeError.value = '';
bannedReasonError.value = ''; bannedReasonError.value = '';
bannedUntilError.value = ''; bannedUntilError.value = '';
// Clear errors for new patrol unit fields
patrolNameError.value = '';
patrolTypeError.value = '';
patrolRadiusError.value = '';
} }
@override @override
@ -213,6 +505,11 @@ final RxBool isFormValid = RxBool(true);
qrCodeController.dispose(); qrCodeController.dispose();
bannedReasonController.dispose(); bannedReasonController.dispose();
bannedUntilController.dispose(); bannedUntilController.dispose();
// Dispose controllers for new patrol unit
patrolNameController.dispose();
patrolTypeController.dispose();
patrolRadiusController.dispose();
super.onClose(); super.onClose();
} }
} }

View File

@ -1,7 +1,9 @@
import 'dart:developer' as dev; import 'dart:developer' as dev;
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
@ -50,6 +52,15 @@ class SelfieVerificationController extends GetxController {
final RxBool bypassLivenessCheck = RxBool(false); final RxBool bypassLivenessCheck = RxBool(false);
final RxBool autoVerifyForDev = RxBool(false); final RxBool autoVerifyForDev = RxBool(false);
// Added a flag to prevent multiple image picker attempts
bool _isImagePickerActive = false;
// Added counter to track auto-verify attempts
int _autoVerifyAttempts = 0;
// Add a GlobalKey for rendering a widget to an image (for dev mode)
final GlobalKey _previewContainer = GlobalKey();
// Constructor // Constructor
SelfieVerificationController({this.idCardController}); SelfieVerificationController({this.idCardController});
@ -94,6 +105,45 @@ class SelfieVerificationController extends GetxController {
Future.microtask(() { Future.microtask(() {
_initializeAfterDependencies(); _initializeAfterDependencies();
}); });
// Add listeners to the dev mode flags to reset state when turned off
ever(bypassLivenessCheck, _handleDevModeChange);
ever(autoVerifyForDev, _handleDevModeChange);
}
// New method to handle changes in dev mode settings
void _handleDevModeChange(bool isEnabled) {
if (!isEnabled) {
// If dev mode is turned off, reset to initial state
Logger().i('Dev mode disabled, resetting state to initial');
_resetToInitialState();
}
}
// New method to reset everything to initial state when dev mode is turned off
void _resetToInitialState() {
// Reset all states to initial values
clearSelfieImage();
resetVerificationState();
_autoVerifyAttempts = 0;
_isImagePickerActive = false;
// Clean up any temporary image files
_cleanupTemporaryFiles();
}
// Method to clean up any temporary image files
Future<void> _cleanupTemporaryFiles() async {
try {
if (selfieImage.value != null &&
File(selfieImage.value!.path).existsSync()) {
// Delete the actual file from storage
await File(selfieImage.value!.path).delete();
Logger().i('Temporary selfie image file deleted');
}
} catch (e) {
Logger().e('Error cleaning up temporary files: $e');
}
} }
void _initializeAfterDependencies() { void _initializeAfterDependencies() {
@ -140,9 +190,13 @@ class SelfieVerificationController extends GetxController {
_processCapturedLivenessImage(); _processCapturedLivenessImage();
} }
// Method to perform liveness detection with improved navigation // Enhanced version of the method to perform liveness detection
void performLivenessDetection() async { void performLivenessDetection() async {
try { try {
// Clear any existing verification data first
clearSelfieImage();
resetVerificationState();
isPerformingLivenessCheck.value = true; isPerformingLivenessCheck.value = true;
autoStartVerification = true; // Set flag for auto verification autoStartVerification = true; // Set flag for auto verification
@ -341,12 +395,28 @@ class SelfieVerificationController extends GetxController {
} }
} }
// Clear selfie image // Clear selfie image - Enhanced to ensure proper cleanup
void clearSelfieImage() { void clearSelfieImage() {
selfieImage.value = null; try {
isSelfieValid.value = false; // Delete any existing image file
hasConfirmedSelfie.value = false; if (selfieImage.value != null) {
selfieError.value = ''; final imagePath = selfieImage.value!.path;
try {
if (File(imagePath).existsSync()) {
File(imagePath).deleteSync();
Logger().i('Deleted selfie image file: $imagePath');
}
} catch (e) {
Logger().e('Error deleting selfie image file: $e');
}
}
} finally {
// Always reset the state values
selfieImage.value = null;
isSelfieValid.value = false;
hasConfirmedSelfie.value = false;
selfieError.value = '';
}
} }
// Confirm the selfie // Confirm the selfie
@ -365,8 +435,9 @@ class SelfieVerificationController extends GetxController {
selfieError.value = ''; selfieError.value = '';
} }
// Reset verification state // Reset verification state - update to make it more thorough
void resetVerificationState() { void resetVerificationState() {
// Reset all related state variables to initial values
isVerifyingFace.value = false; isVerifyingFace.value = false;
isComparingWithIDCard.value = false; isComparingWithIDCard.value = false;
isMatchWithIDCard.value = false; isMatchWithIDCard.value = false;
@ -374,59 +445,47 @@ class SelfieVerificationController extends GetxController {
faceComparisonResult.value = null; faceComparisonResult.value = null;
isLivenessCheckPassed.value = false; isLivenessCheckPassed.value = false;
isPerformingLivenessCheck.value = false; isPerformingLivenessCheck.value = false;
isUploadingSelfie.value = false;
hasConfirmedSelfie.value = false; hasConfirmedSelfie.value = false;
autoStartVerification = false; // Reset auto verification flag autoStartVerification = false;
selfieError.value = ''; selfieError.value = '';
} isSelfieValid.value = false;
// Method to bypass liveness check with a random selfie // Ensure the image is fully reset as well
Future<void> bypassLivenessCheckWithRandomImage() async { if (selfieImage.value != null) {
try { clearSelfieImage();
final logger = Logger();
logger.i('DEV MODE: Bypassing liveness check with random image');
// Start loading state
isPerformingLivenessCheck.value = true;
// Simulate loading time
await Future.delayed(const Duration(seconds: 1));
// Create a temporary file from a bundled asset or generate one
final tempFile = await _createTemporaryImageFile();
if (tempFile != null) {
// Set the selfie image
selfieImage.value = XFile(tempFile.path);
// Set liveness check as passed
isLivenessCheckPassed.value = true;
// Automatically start verification
autoStartVerification = true;
// Log the bypass action
logger.i('DEV MODE: Liveness check bypassed successfully');
// Start face verification
await Future.delayed(const Duration(seconds: 1));
await _simulateSuccessfulVerification();
} else {
logger.e('DEV MODE: Failed to create temporary image file');
selfieError.value = 'Failed to create dummy selfie image';
}
} catch (e) {
Logger().e('DEV MODE: Error bypassing liveness check: $e');
selfieError.value = 'Error bypassing liveness check';
} finally {
isPerformingLivenessCheck.value = false;
} }
} }
// Auto-complete verification for development purposes // Auto-complete verification for development purposes
Future<void> autoCompleteVerification() async { Future<void> autoCompleteVerification() async {
try { try {
// Clear existing data first to ensure fresh state
clearSelfieImage();
resetVerificationState();
// Prevent multiple simultaneous attempts
if (_isImagePickerActive) {
Logger().w(
'DEV MODE: Image picker already active, skipping auto-verification',
);
return;
}
// Check if we've tried too many times
if (_autoVerifyAttempts > 2) {
Logger().w('DEV MODE: Too many auto-verify attempts, using fallback');
await _simulateVerificationWithoutImage();
return;
}
_autoVerifyAttempts++;
_isImagePickerActive = true;
final logger = Logger(); final logger = Logger();
logger.i('DEV MODE: Auto-completing verification'); logger.i(
'DEV MODE: Auto-completing verification (attempt $_autoVerifyAttempts)',
);
// Clear previous states // Clear previous states
clearSelfieImage(); clearSelfieImage();
@ -435,8 +494,8 @@ class SelfieVerificationController extends GetxController {
// Set loading states to show progress // Set loading states to show progress
isPerformingLivenessCheck.value = true; isPerformingLivenessCheck.value = true;
// Create a temporary file from assets or generate one // Create a temporary file using our improved method
final tempFile = await _createTemporaryImageFile(); final tempFile = await _createDevModeImageFile();
if (tempFile != null) { if (tempFile != null) {
// Set the selfie image // Set the selfie image
@ -451,6 +510,7 @@ class SelfieVerificationController extends GetxController {
isVerifyingFace.value = true; isVerifyingFace.value = true;
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
isVerifyingFace.value = false; isVerifyingFace.value = false;
isSelfieValid.value = true;
// Simulate comparison with ID // Simulate comparison with ID
isComparingWithIDCard.value = true; isComparingWithIDCard.value = true;
@ -483,20 +543,88 @@ class SelfieVerificationController extends GetxController {
logger.i('DEV MODE: Auto-verification completed successfully'); logger.i('DEV MODE: Auto-verification completed successfully');
} else { } else {
logger.e('DEV MODE: Failed to create temporary image file'); // If we couldn't create an image file, simulate verification without an image
selfieError.value = 'Failed to create auto-verification image'; logger.w(
isPerformingLivenessCheck.value = false; 'DEV MODE: Failed to create temporary image file, using fallback',
);
await _simulateVerificationWithoutImage();
} }
} catch (e) { } catch (e) {
Logger().e('DEV MODE: Error in auto-verification: $e'); Logger().e('DEV MODE: Error in auto-verification: $e');
selfieError.value = 'Error in auto-verification'; selfieError.value = 'Error in auto-verification';
isPerformingLivenessCheck.value = false; isPerformingLivenessCheck.value = false;
// Try the fallback approach if we had an error
if (_autoVerifyAttempts <= 3) {
Logger().w('DEV MODE: Auto-verify error, attempting fallback');
await _simulateVerificationWithoutImage();
}
} finally {
_isImagePickerActive = false;
}
}
// New method to simulate verification without needing a real image file
Future<void> _simulateVerificationWithoutImage() async {
try {
// Reset all states first
clearErrors();
isPerformingLivenessCheck.value = false;
isVerifyingFace.value = false;
isComparingWithIDCard.value = false;
// Create a colored rectangle image
final tempFile = await _generateColoredRectangleImage();
if (tempFile != null) {
// Set as selfie image
selfieImage.value = XFile(tempFile.path);
// Set all verification flags to success
isLivenessCheckPassed.value = true;
isSelfieValid.value = true;
isMatchWithIDCard.value = true;
matchConfidence.value = 0.99;
// Create a result object
faceComparisonResult.value = FaceComparisonResult(
sourceFace: FaceModel(
imagePath: tempFile.path,
faceId: 'dev_mode_face_id',
),
targetFace: FaceModel(
imagePath: 'dev_mode_target',
faceId: 'dev_mode_target_id',
),
isMatch: true,
confidence: 0.99,
message: 'DEV MODE: Simulated verification',
);
// Auto-confirm
hasConfirmedSelfie.value = true;
Logger().i('DEV MODE: Fallback verification completed successfully');
} else {
// Last resort - just set the flags without an image
Logger().w(
'DEV MODE: Could not create even a fallback image, setting flags only',
);
isLivenessCheckPassed.value = true;
isSelfieValid.value = true;
isMatchWithIDCard.value = true;
hasConfirmedSelfie.value = true;
}
} catch (e) {
Logger().e('DEV MODE: Error in fallback verification: $e');
} }
} }
// Helper method to create a temporary image file from assets or generate one // Helper method to create a temporary image file from assets or generate one
Future<File?> _createTemporaryImageFile() async { Future<File?> _createDevModeImageFile() async {
try { try {
Logger().i('DEV MODE: Creating temporary image file');
// Option 1: Use a bundled asset (if available) // Option 1: Use a bundled asset (if available)
try { try {
final ByteData byteData = await rootBundle.load( final ByteData byteData = await rootBundle.load(
@ -505,33 +633,128 @@ class SelfieVerificationController extends GetxController {
final Uint8List bytes = byteData.buffer.asUint8List(); final Uint8List bytes = byteData.buffer.asUint8List();
final tempDir = await getTemporaryDirectory(); final tempDir = await getTemporaryDirectory();
final tempFile = File('${tempDir.path}/sample_selfie.jpg'); final tempFile = File(
'${tempDir.path}/dev_sample_selfie_${DateTime.now().millisecondsSinceEpoch}.jpg',
);
await tempFile.writeAsBytes(bytes); await tempFile.writeAsBytes(bytes);
Logger().i('DEV MODE: Created image from asset bundle');
return tempFile; return tempFile;
} catch (e) { } catch (e) {
// If no bundled asset, fall back to option 2 // If no bundled asset, fall back to generated image
Logger().i('No bundled selfie asset found, generating random file'); Logger().i(
'DEV MODE: No bundled selfie asset found, will generate an image',
);
} }
// Option 2: Try to use a device camera image if available // Option 2: Generate a simple image programmatically
final ImagePicker picker = ImagePicker(); return await _generateColoredRectangleImage();
final XFile? cameraImage = await picker.pickImage( } catch (e) {
source: ImageSource.gallery, Logger().e('DEV MODE: Error creating dev mode image file: $e');
imageQuality: 50, return null;
); }
}
if (cameraImage != null) { // New method to generate a simple colored rectangle as an image
return File(cameraImage.path); Future<File?> _generateColoredRectangleImage() async {
try {
// Create a simple colored rectangle picture
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final paint = Paint()..color = Colors.blue;
// Draw a face-like shape
canvas.drawRect(Rect.fromLTWH(0, 0, 200, 200), paint);
// Draw some face-like features
final facePaint = Paint()..color = Colors.white;
// Eyes
canvas.drawCircle(const Offset(70, 80), 20, facePaint);
canvas.drawCircle(const Offset(130, 80), 20, facePaint);
// Mouth
canvas.drawOval(Rect.fromLTWH(60, 120, 80, 30), facePaint);
// Convert to image
final picture = recorder.endRecording();
final img = await picture.toImage(200, 200);
final pngBytes = await img.toByteData(format: ui.ImageByteFormat.png);
if (pngBytes != null) {
final tempDir = await getTemporaryDirectory();
final tempFile = File(
'${tempDir.path}/dev_face_${DateTime.now().millisecondsSinceEpoch}.png',
);
await tempFile.writeAsBytes(pngBytes.buffer.asUint8List());
Logger().i('DEV MODE: Generated colored rectangle image successfully');
return tempFile;
} }
// If both fail, return null and handle in calling method
return null; return null;
} catch (e) { } catch (e) {
Logger().e('Error creating temporary image file: $e'); Logger().e('DEV MODE: Error generating colored rectangle: $e');
return null; return null;
} }
} }
// Method to bypass liveness check with a generated selfie
Future<void> bypassLivenessCheckWithRandomImage() async {
try {
// Clear existing data first
clearSelfieImage();
resetVerificationState();
// Avoid multiple simultaneous attempts
if (_isImagePickerActive) {
Logger().w('DEV MODE: Image picker already active, skipping bypass');
return;
}
_isImagePickerActive = true;
final logger = Logger();
logger.i('DEV MODE: Bypassing liveness check with generated image');
// Start loading state
isPerformingLivenessCheck.value = true;
// Simulate loading time
await Future.delayed(const Duration(seconds: 1));
// Create a temporary file using dev mode method
final tempFile = await _createDevModeImageFile();
if (tempFile != null) {
// Set the selfie image
selfieImage.value = XFile(tempFile.path);
// Set liveness check as passed
isLivenessCheckPassed.value = true;
// Automatically start verification
autoStartVerification = true;
// Log the bypass action
logger.i('DEV MODE: Liveness check bypassed successfully');
// Start face verification
await Future.delayed(const Duration(seconds: 1));
await _simulateSuccessfulVerification();
} else {
// Try the fallback approach
logger.w('DEV MODE: Failed with primary method, trying fallback');
await _simulateVerificationWithoutImage();
}
} catch (e) {
Logger().e('DEV MODE: Error bypassing liveness check: $e');
selfieError.value = 'Error bypassing liveness check';
// Try the fallback approach
await _simulateVerificationWithoutImage();
} finally {
isPerformingLivenessCheck.value = false;
_isImagePickerActive = false;
}
}
// Simulate successful verification for development purposes // Simulate successful verification for development purposes
Future<void> _simulateSuccessfulVerification() async { Future<void> _simulateSuccessfulVerification() async {
try { try {

View File

@ -5,7 +5,6 @@ import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/reg
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/id-card-verification/id_card_verification_step.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/step/id-card-verification/id_card_verification_step.dart';
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/identity-verification/identity_verification_step.dart'; import 'package:sigap/src/features/auth/presentasion/pages/signup/step/identity-verification/identity_verification_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/officer-information/officer_info_step.dart';
import 'package:sigap/src/features/auth/presentasion/pages/signup/step/officer-information/unit_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/features/auth/presentasion/widgets/auth_button.dart';
@ -167,12 +166,6 @@ class FormRegistrationScreen extends StatelessWidget {
return isOfficer return isOfficer
? const OfficerInfoStep() ? const OfficerInfoStep()
: const IdentityVerificationStep(); : const IdentityVerificationStep();
case 4:
// This step only exists for officers
if (isOfficer) {
return const UnitInfoStep();
}
return const SizedBox.shrink();
default: default:
return const SizedBox.shrink(); return const SizedBox.shrink();
} }

View File

@ -187,10 +187,7 @@ class IdCardVerificationStep extends StatelessWidget {
"Your photo and all text should be clearly visible", "Your photo and all text should be clearly visible",
"Avoid using flash to prevent glare", "Avoid using flash to prevent glare",
], ],
backgroundColor: TColors.primary.withOpacity(0.1),
textColor: TColors.primary,
iconColor: TColors.primary,
borderColor: TColors.primary.withOpacity(0.3),
); );
} }

View File

@ -76,11 +76,7 @@ class FaceVerificationSection extends StatelessWidget {
'Remove glasses and face coverings', 'Remove glasses and face coverings',
'Face the camera directly without tilting your head', 'Face the camera directly without tilting your head',
], ],
backgroundColor: TColors.primary.withOpacity(0.1),
textColor: TColors.primary,
iconColor: TColors.primary,
leadingIcon: Icons.face,
borderColor: TColors.primary.withOpacity(0.3),
); );
} }

View File

@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart'; import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/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/features/auth/presentasion/pages/signup/step/officer-information/patrol_unit_selection_screen.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/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/sizes.dart'; import 'package:sigap/src/utils/constants/sizes.dart';
import 'package:sigap/src/utils/validators/validation.dart'; import 'package:sigap/src/utils/validators/validation.dart';
@ -29,100 +31,574 @@ class OfficerInfoStep extends StatelessWidget {
const SizedBox(height: TSizes.spaceBtwItems), const SizedBox(height: TSizes.spaceBtwItems),
// Unit ID field
Obx(
() => CustomTextField(
label: 'Unit ID',
controller: controller.unitIdController,
validator: (v) => TValidators.validateUserInput('Unit ID', v, 20),
errorText: controller.unitIdError.value,
textInputAction: TextInputAction.next,
hintText: 'e.g., POLRES01',
onChanged: (value) {
controller.unitIdController.text = value;
controller.unitIdError.value = '';
},
),
),
// Patrol Unit ID field
Obx(
() => CustomTextField(
label: 'Patrol Unit ID',
controller: controller.patrolUnitIdController,
validator:
(v) => TValidators.validateUserInput(
'Patrol Unit ID',
v,
100,
required: true,
),
errorText: controller.patrolUnitIdError.value,
textInputAction: TextInputAction.next,
hintText: 'e.g., PATROL01',
onChanged: (value) {
controller.patrolUnitIdController.text = value;
controller.patrolUnitIdError.value = '';
},
),
),
// NRP field // NRP field
CustomTextField(
label: 'NRP',
controller: controller.nrpController,
validator: TValidators.validateNRP,
errorText: controller.nrpError.value,
textInputAction: TextInputAction.next,
keyboardType: TextInputType.number,
hintText: 'e.g., 123456789',
onChanged: (value) {
controller.nrpController.text = value;
controller.nrpError.value = '';
},
),
Obx( Obx(
() => CustomTextField( () =>
label: 'NRP', controller.nrpError.value.isNotEmpty
controller: controller.nrpController, ? Padding(
validator: TValidators.validateNRP, padding: const EdgeInsets.only(top: 8.0),
errorText: controller.nrpError.value, child: Text(
textInputAction: TextInputAction.next, controller.nrpError.value,
keyboardType: TextInputType.number, style: TextStyle(color: Colors.red[700], fontSize: 12),
hintText: 'e.g., 123456789', ),
onChanged: (value) { )
controller.nrpController.text = value; : const SizedBox.shrink(),
controller.nrpError.value = '';
},
),
), ),
// Rank field // Rank field
CustomTextField(
label: 'Rank',
controller: controller.rankController,
validator: TValidators.validateRank,
errorText: controller.rankError.value,
textInputAction: TextInputAction.next,
hintText: 'e.g., Captain',
onChanged: (value) {
controller.rankController.text = value;
controller.rankError.value = '';
},
),
Obx( Obx(
() => CustomTextField( () =>
label: 'Rank', controller.rankError.value.isNotEmpty
controller: controller.rankController, ? Padding(
validator: TValidators.validateRank, padding: const EdgeInsets.only(top: 8.0),
errorText: controller.rankError.value, child: Text(
textInputAction: TextInputAction.next, controller.rankError.value,
hintText: 'e.g., Captain', style: TextStyle(color: Colors.red[700], fontSize: 12),
onChanged: (value) { ),
controller.rankController.text = value; )
controller.rankError.value = ''; : const SizedBox.shrink(),
},
),
), ),
// Position field // Position field
CustomTextField(
label: 'Position',
controller: controller.positionController,
validator:
(v) => TValidators.validateUserInput(
'Position',
v,
100,
required: true,
),
errorText: controller.positionError.value,
textInputAction: TextInputAction.next,
hintText: 'e.g., Head of Unit',
onChanged: (value) {
controller.positionController.text = value;
controller.positionError.value = '';
},
),
Obx( Obx(
() => CustomTextField( () =>
label: 'Position', controller.positionError.value.isNotEmpty
controller: controller.positionController, ? Padding(
validator: padding: const EdgeInsets.only(top: 8.0),
(v) => TValidators.validateUserInput( child: Text(
'Position', controller.positionError.value,
v, style: TextStyle(color: Colors.red[700], fontSize: 12),
100, ),
required: true, )
), : const SizedBox.shrink(),
errorText: controller.positionError.value, ),
textInputAction: TextInputAction.done,
hintText: 'e.g., Head of Unit', const SizedBox(height: TSizes.spaceBtwSections),
onChanged: (value) {
controller.positionController.text = value; // Unit Selection Section
controller.positionError.value = ''; const FormSectionHeader(
}, title: 'Unit Selection',
), subtitle: 'Select your police unit',
),
const SizedBox(height: TSizes.spaceBtwItems),
// Unit dropdown
_buildUnitDropdown(controller),
const SizedBox(height: TSizes.spaceBtwSections),
// Patrol Unit Section - Simplified to show only current selection and a button to navigate
const FormSectionHeader(
title: 'Patrol Unit',
subtitle: 'Select or create your patrol unit',
),
const SizedBox(height: TSizes.spaceBtwItems),
// Display selected patrol unit (if any)
Builder(
builder: (context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final theme = Theme.of(context);
return GetX<OfficerInfoController>(
builder:
(controller) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (controller.selectedPatrolUnitName.isNotEmpty)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: theme.primaryColor.withOpacity(
isDark ? 0.2 : 0.1,
),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: theme.primaryColor),
),
child: Row(
children: [
Icon(
controller.selectedPatrolType.value ==
PatrolUnitType.car
? Icons.directions_car
: Icons.motorcycle,
color: theme.primaryColor,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'Selected Patrol Unit',
style: TextStyle(
color:
isDark
? Colors.grey[400]
: Colors.grey[600],
fontSize: 12,
),
),
Text(
controller.selectedPatrolUnitName.value,
style: TextStyle(
fontWeight: FontWeight.bold,
color:
isDark
? Colors.white
: Colors.black87,
),
),
],
),
),
],
),
),
const SizedBox(height: 16),
// Configure Patrol Unit button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed:
() => _navigateToPatrolUnitSelectionScreen(
controller,
),
style: ElevatedButton.styleFrom(
backgroundColor: theme.primaryColor,
foregroundColor:
isDark ? Colors.black : Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
icon: Icon(
controller.patrolUnitIdController.text.isEmpty
? Icons.add_circle_outline
: Icons.edit,
),
label: Text(
controller.patrolUnitIdController.text.isEmpty
? 'Configure Patrol Unit'
: 'Change Patrol Unit',
),
),
),
Obx(
() =>
controller.patrolUnitIdError.value.isNotEmpty
? Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
controller.patrolUnitIdError.value,
style: TextStyle(
color: TColors.error,
fontSize: 12,
),
),
)
: const SizedBox.shrink(),
),
],
),
);
},
), ),
], ],
), ),
); );
} }
// Navigate to the patrol unit selection screen
void _navigateToPatrolUnitSelectionScreen(OfficerInfoController controller) {
// Check if a unit is selected first
if (controller.unitIdController.text.isEmpty) {
Get.snackbar(
'Unit Required',
'Please select a unit before configuring patrol unit',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
return;
}
Get.to(() => PatrolUnitSelectionScreen());
}
// Build unit dropdown selection
Widget _buildUnitDropdown(OfficerInfoController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Label - using context directly
Builder(
builder: (context) {
return Text(
'Select Unit:',
style: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500),
);
},
),
const SizedBox(height: TSizes.sm),
// Dropdown using Builder to access current context (and theme)
Builder(
builder: (context) {
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(
horizontal: TSizes.md,
vertical: TSizes.md,
),
decoration: BoxDecoration(
border: Border.all(color: theme.dividerColor),
borderRadius: borderRadius,
color: fillColor,
),
child: Center(
child: Padding(
padding: const EdgeInsets.all(TSizes.sm),
child: CircularProgressIndicator(
color: theme.primaryColor,
),
),
),
);
}
if (controller.availableUnits.isEmpty) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: TSizes.md,
vertical: TSizes.md,
),
decoration: BoxDecoration(
border: Border.all(color: theme.dividerColor),
borderRadius: borderRadius,
color: fillColor,
),
child: Text(
'No units available',
style: theme.textTheme.bodyMedium?.copyWith(
color: isDark ? Colors.grey[400] : Colors.grey[600],
),
),
);
}
// Get the selected unit (if any)
final selectedUnit = controller.availableUnits.firstWhereOrNull(
(unit) => unit.codeUnit == controller.unitIdController.text,
);
return Column(
children: [
// Dropdown Selection Button
GestureDetector(
onTap: () {
// Toggle dropdown visibility
controller.isUnitDropdownOpen.toggle();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: TSizes.md,
vertical: TSizes.md,
),
decoration: BoxDecoration(
border: Border.all(
color:
controller.isUnitDropdownOpen.value
? theme.primaryColor
: theme.dividerColor,
width:
controller.isUnitDropdownOpen.value ? 1.5 : 1,
),
borderRadius: borderRadius,
color: fillColor,
),
child: Row(
children: [
Expanded(
child: Text(
selectedUnit != null
? '${selectedUnit.name} (${selectedUnit.type.name})'
: 'Select Unit',
style: theme.textTheme.bodyMedium?.copyWith(
color:
selectedUnit != null
? theme.textTheme.bodyMedium?.color
: (isDark
? Colors.grey[400]
: Colors.grey[600]),
),
),
),
Icon(
controller.isUnitDropdownOpen.value
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
color:
controller.isUnitDropdownOpen.value
? theme.primaryColor
: (isDark
? Colors.grey[400]
: Colors.grey[600]),
),
],
),
),
),
// Dropdown Options
if (controller.isUnitDropdownOpen.value)
Container(
margin: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
color: fillColor,
borderRadius: borderRadius,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(
isDark ? 0.3 : 0.1,
),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
border: Border.all(color: theme.dividerColor),
),
constraints: const BoxConstraints(maxHeight: 250),
child: ClipRRect(
borderRadius: borderRadius,
child: ListView.builder(
shrinkWrap: true,
padding: EdgeInsets.zero,
itemCount: controller.availableUnits.length,
itemBuilder: (context, index) {
final unit = controller.availableUnits[index];
final isSelected =
unit.codeUnit ==
controller.unitIdController.text;
return GestureDetector(
onTap: () {
controller.onUnitSelected(unit);
controller.isUnitDropdownOpen.value = false;
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: TSizes.md,
vertical: TSizes.md - 2,
),
decoration: BoxDecoration(
color:
isSelected
? theme.primaryColor.withOpacity(
isDark ? 0.2 : 0.1,
)
: Colors.transparent,
border:
index <
controller
.availableUnits
.length -
1
? Border(
bottom: BorderSide(
color: theme.dividerColor
.withOpacity(0.5),
width: 0.5,
),
)
: null,
),
child: Row(
children: [
// Unit Icon
Icon(
isSelected
? Icons.shield
: Icons.shield_outlined,
color:
isSelected
? theme.primaryColor
: (isDark
? Colors.grey[400]
: Colors.grey[600]),
size: 20,
),
const SizedBox(width: 12),
// Unit Name
Expanded(
child: Text(
'${unit.name} (${unit.type.name})',
style: theme.textTheme.bodyMedium
?.copyWith(
color:
isSelected
? theme.primaryColor
: theme
.textTheme
.bodyMedium
?.color,
fontWeight:
isSelected
? FontWeight.bold
: FontWeight.normal,
),
),
),
// Checkmark for selected item
if (isSelected)
Icon(
Icons.check,
color: theme.primaryColor,
size: 18,
),
],
),
),
);
},
),
),
),
// Selected unit display
if (selectedUnit != null &&
!controller.isUnitDropdownOpen.value)
Container(
margin: const EdgeInsets.only(
top: TSizes.spaceBtwInputFields,
),
padding: const EdgeInsets.all(TSizes.md),
decoration: BoxDecoration(
color: theme.primaryColor.withOpacity(
isDark ? 0.2 : 0.1,
),
borderRadius: borderRadius,
border: Border.all(color: theme.primaryColor),
),
child: Row(
children: [
Icon(
Icons.shield_outlined,
color: theme.primaryColor,
),
const SizedBox(width: TSizes.sm),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selected Unit',
style: theme.textTheme.labelSmall?.copyWith(
color:
isDark
? Colors.grey[400]
: Colors.grey[600],
),
),
Text(
'${selectedUnit.name} (${selectedUnit.type.name})',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
],
),
),
Icon(
Icons.check_circle,
color: theme.primaryColor,
size: 20,
),
],
),
),
// Error message
if (controller.unitIdError.value.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
controller.unitIdError.value,
style: TextStyle(color: TColors.error, fontSize: 12),
),
),
if (selectedUnit == null ||
controller.unitIdError.value.isEmpty)
const SizedBox(height: TSizes.spaceBtwInputFields),
],
);
},
);
},
),
],
);
}
} }

View File

@ -0,0 +1,485 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.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/sizes.dart';
import 'package:sigap/src/utils/validators/validation.dart';
class PatrolUnitSelectionScreen extends StatelessWidget {
PatrolUnitSelectionScreen({super.key});
final controller = Get.find<OfficerInfoController>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Configure Patrol Unit'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Get.back(),
),
),
body: Padding(
padding: const EdgeInsets.all(TSizes.defaultSpace),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Unit info display
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.business, color: TColors.primary),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Selected Unit',
style: TextStyle(
color: Colors.grey[600],
fontSize: 12,
),
),
Obx(
() => Text(
controller.selectedUnitName.value,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
],
),
),
],
),
),
const SizedBox(height: TSizes.spaceBtwSections),
// Patrol Type Selection
const Text(
'Patrol Type:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: TSizes.spaceBtwInputFields / 2),
GetX<OfficerInfoController>(
builder:
(controller) => Row(
children: [
Expanded(
child: InkWell(
onTap: () {
controller.setPatrolUnitType(PatrolUnitType.car);
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color:
controller.selectedPatrolType.value ==
PatrolUnitType.car
? TColors.primary.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
controller.selectedPatrolType.value ==
PatrolUnitType.car
? TColors.primary
: Colors.grey,
),
),
child: Column(
children: [
Icon(
Icons.directions_car,
color:
controller.selectedPatrolType.value ==
PatrolUnitType.car
? TColors.primary
: Colors.grey,
size: 32,
),
const SizedBox(height: TSizes.sm),
const Text('Car'),
],
),
),
),
),
const SizedBox(width: TSizes.spaceBtwInputFields),
Expanded(
child: InkWell(
onTap: () {
controller.setPatrolUnitType(
PatrolUnitType.motorcycle,
);
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color:
controller.selectedPatrolType.value ==
PatrolUnitType.motorcycle
? TColors.primary.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
controller.selectedPatrolType.value ==
PatrolUnitType.motorcycle
? TColors.primary
: Colors.grey,
),
),
child: Column(
children: [
Icon(
Icons.motorcycle,
color:
controller.selectedPatrolType.value ==
PatrolUnitType.motorcycle
? TColors.primary
: Colors.grey,
size: 32,
),
const SizedBox(height: TSizes.sm),
const Text('Motorcycle'),
],
),
),
),
),
],
),
),
const SizedBox(height: TSizes.spaceBtwSections),
// Selection Mode Tabs
const Text(
'Select option:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: TSizes.spaceBtwInputFields / 2),
GetX<OfficerInfoController>(
builder:
(controller) => Row(
children: [
_buildModeTab(
controller,
PatrolSelectionMode.individual,
'Individual',
Icons.person,
),
_buildModeTab(
controller,
PatrolSelectionMode.group,
'Group',
Icons.group,
),
_buildModeTab(
controller,
PatrolSelectionMode.createNew,
'Create New',
Icons.add_circle,
),
],
),
),
const SizedBox(height: TSizes.spaceBtwInputFields),
// Patrol Unit Selection/Creation based on mode
Expanded(
child: GetX<OfficerInfoController>(
builder: (controller) {
switch (controller.patrolSelectionMode.value) {
case PatrolSelectionMode.individual:
case PatrolSelectionMode.group:
return _buildExistingPatrolUnitSelection(controller);
case PatrolSelectionMode.createNew:
return _buildCreatePatrolUnitForm(controller);
}
},
),
),
// Confirm button
Padding(
padding: const EdgeInsets.symmetric(
vertical: TSizes.defaultSpace,
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (controller.patrolSelectionMode.value ==
PatrolSelectionMode.createNew) {
controller.createNewPatrolUnit().then((success) {
if (success) {
Get.back();
}
});
} else if (controller
.patrolUnitIdController
.text
.isNotEmpty) {
// Selection mode - just go back if a patrol unit is selected
Get.back();
} else {
Get.snackbar(
'Selection Required',
'Please select a patrol unit or create a new one',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.withOpacity(0.1),
colorText: Colors.red,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: TColors.primary,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Confirm Selection'),
),
),
),
],
),
),
);
}
// Helper method to build a selection mode tab
Widget _buildModeTab(
OfficerInfoController controller,
PatrolSelectionMode mode,
String label,
IconData icon,
) {
return Expanded(
child: InkWell(
onTap: () => controller.setPatrolSelectionMode(mode),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color:
controller.patrolSelectionMode.value == mode
? TColors.primary.withOpacity(0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
controller.patrolSelectionMode.value == mode
? TColors.primary
: Colors.grey,
),
),
child: Column(
children: [
Icon(
icon,
color:
controller.patrolSelectionMode.value == mode
? TColors.primary
: Colors.grey,
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
color:
controller.patrolSelectionMode.value == mode
? TColors.primary
: Colors.grey,
),
),
],
),
),
),
);
}
// Build existing patrol unit selection list
Widget _buildExistingPatrolUnitSelection(OfficerInfoController controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Select Patrol Unit:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: TSizes.spaceBtwInputFields / 2),
GetX<OfficerInfoController>(
builder: (controller) {
if (controller.isLoadingPatrolUnits.value) {
return const Expanded(
child: Center(child: CircularProgressIndicator()),
);
}
if (controller.unitIdController.text.isEmpty) {
return const Expanded(
child: Center(child: Text('Please select a unit first')),
);
}
final filteredUnits = controller.getFilteredPatrolUnits();
if (filteredUnits.isEmpty) {
return const Expanded(
child: Center(
child: Text('No patrol units available for this type'),
),
);
}
return Expanded(
child: ListView.builder(
itemCount: filteredUnits.length,
itemBuilder: (context, index) {
final patrolUnit = filteredUnits[index];
final isSelected =
patrolUnit.id == controller.patrolUnitIdController.text;
return Card(
elevation: isSelected ? 2 : 0,
color: isSelected ? TColors.primary.withOpacity(0.1) : null,
margin: const EdgeInsets.symmetric(vertical: 4),
child: ListTile(
title: Text(
patrolUnit.name,
style: TextStyle(
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
),
),
subtitle: Text(
'Members: ${patrolUnit.members?.length ?? 0}',
),
leading: Icon(
patrolUnit.type.toLowerCase() == 'car'
? Icons.directions_car
: Icons.motorcycle,
color: isSelected ? TColors.primary : null,
),
trailing:
isSelected
? const Icon(
Icons.check_circle,
color: TColors.primary,
)
: null,
selected: isSelected,
onTap: () => controller.joinPatrolUnit(patrolUnit),
),
);
},
),
);
},
),
],
);
}
// Build create new patrol unit form
Widget _buildCreatePatrolUnitForm(OfficerInfoController controller) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Create New Patrol Unit:',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: TSizes.spaceBtwInputFields),
// Patrol Name Field
CustomTextField(
label: 'Patrol Unit Name',
controller: controller.patrolNameController,
validator:
(v) => TValidators.validateUserInput('Patrol Unit Name', v, 30),
errorText: controller.patrolNameError.value,
textInputAction: TextInputAction.next,
hintText: 'e.g., Alpha Team',
onChanged: (value) {
controller.patrolNameController.text = value;
controller.patrolNameError.value = '';
},
),
GetX<OfficerInfoController>(
builder:
(controller) =>
controller.patrolNameError.value.isNotEmpty
? Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
controller.patrolNameError.value,
style: TextStyle(
color: Colors.red[700],
fontSize: 12,
),
),
)
: const SizedBox.shrink(),
),
// Patrol Radius Field
CustomTextField(
label: 'Patrol Radius (in meters)',
controller: controller.patrolRadiusController,
validator: (v) {
if (v == null || v.isEmpty) {
return 'Radius is required';
}
try {
final radius = double.parse(v);
if (radius <= 0) {
return 'Radius must be greater than 0';
}
} catch (e) {
return 'Please enter a valid number';
}
return null;
},
errorText: controller.patrolRadiusError.value,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
hintText: 'e.g., 500',
onChanged: (value) {
controller.patrolRadiusController.text = value;
controller.patrolRadiusError.value = '';
},
),
GetX<OfficerInfoController>(
builder:
(controller) =>
controller.patrolRadiusError.value.isNotEmpty
? Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
controller.patrolRadiusError.value,
style: TextStyle(
color: Colors.red[700],
fontSize: 12,
),
),
)
: const SizedBox.shrink(),
),
],
),
);
}
}

View File

@ -10,6 +10,7 @@ import 'package:sigap/src/shared/widgets/verification/validation_message_card.da
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';
import 'package:sigap/src/utils/helpers/helper_functions.dart'; import 'package:sigap/src/utils/helpers/helper_functions.dart';
import 'package:sigap/src/utils/popups/loaders.dart';
// Enum untuk tracking status verifikasi // Enum untuk tracking status verifikasi
enum VerificationStatus { enum VerificationStatus {
@ -33,6 +34,8 @@ class SelfieVerificationStep extends StatelessWidget {
final mainController = Get.find<FormRegistrationController>(); final mainController = Get.find<FormRegistrationController>();
final facialVerificationService = FacialVerificationService.instance; final facialVerificationService = FacialVerificationService.instance;
final isDark = THelperFunctions.isDarkMode(context);
// Check if we need to update the skip verification flag from arguments // Check if we need to update the skip verification flag from arguments
final dynamic args = Get.arguments; final dynamic args = Get.arguments;
if (args != null && if (args != null &&
@ -74,27 +77,29 @@ class SelfieVerificationStep extends StatelessWidget {
BuildContext context = Get.context!; BuildContext context = Get.context!;
final controller = Get.find<SelfieVerificationController>(); final controller = Get.find<SelfieVerificationController>();
final isDark = THelperFunctions.isDarkMode(context);
final warningColor = isDark ? Colors.amber : TColors.warning;
return Container( return Container(
margin: const EdgeInsets.only(bottom: TSizes.spaceBtwItems), margin: const EdgeInsets.only(bottom: TSizes.spaceBtwItems),
padding: const EdgeInsets.all(TSizes.sm), padding: const EdgeInsets.all(TSizes.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.amber.withOpacity(0.1), color: warningColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(TSizes.borderRadiusSm), borderRadius: BorderRadius.circular(TSizes.borderRadiusSm),
border: Border.all(color: Colors.amber), border: Border.all(color: warningColor),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
const Icon(Icons.code, color: Colors.amber, size: TSizes.iconSm), Icon(Icons.code, color: warningColor, size: TSizes.iconSm),
const SizedBox(width: TSizes.xs), const SizedBox(width: TSizes.xs),
Expanded( Expanded(
child: Text( child: Text(
'Development Mode', 'Development Mode',
style: Theme.of(context).textTheme.labelMedium?.copyWith( style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Colors.amber, color: warningColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
@ -111,7 +116,7 @@ class SelfieVerificationStep extends StatelessWidget {
'Bypass liveness check', 'Bypass liveness check',
style: Theme.of( style: Theme.of(
context, context,
).textTheme.labelSmall?.copyWith(color: Colors.amber), ).textTheme.labelSmall?.copyWith(color: warningColor),
), ),
), ),
Obx( Obx(
@ -120,23 +125,26 @@ class SelfieVerificationStep extends StatelessWidget {
onChanged: (value) { onChanged: (value) {
controller.bypassLivenessCheck.value = value; controller.bypassLivenessCheck.value = value;
if (value) { if (value) {
ScaffoldMessenger.of(context).showSnackBar( TLoaders.errorSnackBar(
const SnackBar( title: 'Bypass Enabled',
content: Text('Liveness check will be bypassed'), message: 'Liveness check will be skipped',
backgroundColor: Colors.orange, );
duration: Duration(seconds: 2), } else {
), // When turning off, show a notification
TLoaders.infoSnackBar(
title: 'Bypass Disabled',
message: 'Liveness check will be performed',
); );
} }
}, },
activeColor: Colors.amber, activeColor: warningColor,
activeTrackColor: Colors.amber.withOpacity(0.5), activeTrackColor: warningColor.withOpacity(0.5),
), ),
), ),
], ],
), ),
// Auto-verify toggle (new) // Auto-verify toggle (with updated toggle handler)
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -144,7 +152,7 @@ class SelfieVerificationStep extends StatelessWidget {
'Auto-verify (auto-pass all checks)', 'Auto-verify (auto-pass all checks)',
style: Theme.of( style: Theme.of(
context, context,
).textTheme.labelSmall?.copyWith(color: Colors.amber), ).textTheme.labelSmall?.copyWith(color: warningColor),
), ),
), ),
Obx( Obx(
@ -153,14 +161,9 @@ class SelfieVerificationStep extends StatelessWidget {
onChanged: (value) { onChanged: (value) {
controller.autoVerifyForDev.value = value; controller.autoVerifyForDev.value = value;
if (value) { if (value) {
ScaffoldMessenger.of(context).showSnackBar( TLoaders.errorSnackBar(
const SnackBar( title: 'Auto-verify Enabled',
content: Text( message: 'All checks will be auto-passed',
'All verification steps will be auto-passed',
),
backgroundColor: Colors.deepOrange,
duration: Duration(seconds: 2),
),
); );
// If auto-verify is enabled, also enable bypass // If auto-verify is enabled, also enable bypass
@ -174,10 +177,17 @@ class SelfieVerificationStep extends StatelessWidget {
controller.autoCompleteVerification(); controller.autoCompleteVerification();
}); });
} }
} else {
// When turning off auto-verify, show notification
TLoaders.infoSnackBar(
title: 'Auto-verify Disabled',
message:
'You will need to complete all checks manually',
);
} }
}, },
activeColor: Colors.deepOrange, activeColor: TColors.error,
activeTrackColor: Colors.deepOrange.withOpacity(0.5), activeTrackColor: TColors.error.withOpacity(0.5),
), ),
), ),
], ],
@ -187,7 +197,7 @@ class SelfieVerificationStep extends StatelessWidget {
Text( Text(
'Warning: Only use in development environment!', 'Warning: Only use in development environment!',
style: Theme.of(context).textTheme.labelSmall?.copyWith( style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.red.shade300, color: TColors.error,
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
), ),
), ),
@ -384,7 +394,7 @@ class SelfieVerificationStep extends StatelessWidget {
Color _getTextColor(bool isCompleted, bool isActive, bool isDark) { Color _getTextColor(bool isCompleted, bool isActive, bool isDark) {
if (isCompleted || isActive) { if (isCompleted || isActive) {
return TColors.primary; return isDark ? TColors.light : TColors.primary;
} }
return isDark ? TColors.grey : Colors.grey.shade600; return isDark ? TColors.grey : Colors.grey.shade600;
@ -478,6 +488,13 @@ class SelfieVerificationStep extends StatelessWidget {
}); });
} }
final devAccentColor = TColors.error;
final warningColor = isDark ? Colors.amber : TColors.warning;
final bgColor = isDark ? TColors.darkerGrey : TColors.light;
final borderColor = isDark ? TColors.darkGrey : TColors.grey;
final textColor = isDark ? TColors.light : TColors.dark;
final secondaryTextColor = isDark ? TColors.lightGrey : TColors.darkGrey;
return Column( return Column(
children: [ children: [
// Add auto-verify badge when enabled // Add auto-verify badge when enabled
@ -491,8 +508,8 @@ class SelfieVerificationStep extends StatelessWidget {
horizontal: TSizes.sm, horizontal: TSizes.sm,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.deepOrange.withOpacity(0.1), color: devAccentColor.withOpacity(0.1),
border: Border.all(color: Colors.deepOrange.withOpacity(0.5)), border: Border.all(color: devAccentColor.withOpacity(0.5)),
borderRadius: BorderRadius.circular(TSizes.borderRadiusSm), borderRadius: BorderRadius.circular(TSizes.borderRadiusSm),
), ),
child: Row( child: Row(
@ -501,13 +518,13 @@ class SelfieVerificationStep extends StatelessWidget {
Icon( Icon(
Icons.developer_mode, Icons.developer_mode,
size: TSizes.iconSm, size: TSizes.iconSm,
color: Colors.deepOrange, color: devAccentColor,
), ),
const SizedBox(width: TSizes.xs), const SizedBox(width: TSizes.xs),
Text( Text(
'Auto-Verification Enabled', 'Auto-Verification Enabled',
style: TextStyle( style: TextStyle(
color: Colors.deepOrange, color: devAccentColor,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 12, fontSize: 12,
), ),
@ -521,12 +538,9 @@ class SelfieVerificationStep extends StatelessWidget {
width: double.infinity, width: double.infinity,
height: 200, height: 200,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDark ? Colors.grey.shade900 : Colors.grey.shade50, color: bgColor,
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd), borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
border: Border.all( border: Border.all(color: borderColor, width: 2),
color: isDark ? Colors.grey.shade800 : Colors.grey.shade300,
width: 2,
),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@ -534,7 +548,7 @@ class SelfieVerificationStep extends StatelessWidget {
Icon( Icon(
Icons.face_retouching_natural, Icons.face_retouching_natural,
size: 60, size: 60,
color: Colors.grey.shade400, color: borderColor,
), ),
const SizedBox(height: TSizes.md), const SizedBox(height: TSizes.md),
Text( Text(
@ -542,13 +556,13 @@ class SelfieVerificationStep extends StatelessWidget {
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Colors.grey.shade600, color: textColor,
), ),
), ),
const SizedBox(height: TSizes.sm), const SizedBox(height: TSizes.sm),
Text( Text(
'Tap the button below to start', 'Tap the button below to start',
style: TextStyle(fontSize: 14, color: Colors.grey.shade500), style: TextStyle(fontSize: 14, color: secondaryTextColor),
), ),
], ],
), ),
@ -600,19 +614,19 @@ class SelfieVerificationStep extends StatelessWidget {
}, },
icon: Icon( icon: Icon(
Icons.developer_mode, Icons.developer_mode,
color: Colors.amber, color: warningColor,
size: TSizes.iconSm, size: TSizes.iconSm,
), ),
label: Text( label: Text(
'DEV: Use Random Selfie & Skip Verification', 'DEV: Use Random Selfie & Skip Verification',
style: TextStyle(color: Colors.amber), style: TextStyle(color: warningColor),
), ),
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: TSizes.md, horizontal: TSizes.md,
vertical: TSizes.xs, vertical: TSizes.xs,
), ),
backgroundColor: Colors.amber.withOpacity(0.1), backgroundColor: warningColor.withOpacity(0.1),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(
TSizes.borderRadiusSm, TSizes.borderRadiusSm,
@ -635,8 +649,8 @@ class SelfieVerificationStep extends StatelessWidget {
icon: const Icon(Icons.skip_next), icon: const Icon(Icons.skip_next),
label: const Text('DEV: Skip & Auto-Complete Verification'), label: const Text('DEV: Skip & Auto-Complete Verification'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepOrange, backgroundColor: devAccentColor,
foregroundColor: Colors.white, foregroundColor: TColors.white,
minimumSize: const Size(double.infinity, 45), minimumSize: const Size(double.infinity, 45),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(TSizes.buttonRadius), borderRadius: BorderRadius.circular(TSizes.buttonRadius),
@ -648,15 +662,20 @@ class SelfieVerificationStep extends StatelessWidget {
} }
Widget _buildLivenessState(SelfieVerificationController controller) { Widget _buildLivenessState(SelfieVerificationController controller) {
final isDark = THelperFunctions.isDarkMode(Get.context!); BuildContext context = Get.context!;
final isDark = THelperFunctions.isDarkMode(context);
final bgColor = isDark ? TColors.darkerGrey : TColors.light;
final borderColor = isDark ? TColors.darkGrey : TColors.grey;
final secondaryTextColor = isDark ? TColors.lightGrey : TColors.darkGrey;
return Container( return Container(
width: double.infinity, width: double.infinity,
height: 200, height: 200,
decoration: BoxDecoration( decoration: BoxDecoration(
color: isDark ? Colors.grey.shade900 : Colors.grey.shade50, color: bgColor,
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd), borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
border: Border.all( border: Border.all(
color: isDark ? Colors.grey.shade800 : Colors.grey.shade300, color: borderColor,
width: 2, width: 2,
), ),
), ),
@ -683,7 +702,7 @@ class SelfieVerificationStep extends StatelessWidget {
const SizedBox(height: TSizes.sm), const SizedBox(height: TSizes.sm),
Text( Text(
'Please follow the on-screen instructions', 'Please follow the on-screen instructions',
style: TextStyle(fontSize: 14, color: Colors.grey.shade600), style: TextStyle(fontSize: 14, color: secondaryTextColor),
), ),
], ],
), ),
@ -755,7 +774,7 @@ class SelfieVerificationStep extends StatelessWidget {
isLoading: true, isLoading: true,
title: 'Detecting Face', title: 'Detecting Face',
icon: Icons.face_retouching_natural, icon: Icons.face_retouching_natural,
customColor: Colors.blue, customColor: TColors.primary,
); );
case VerificationStatus.comparingWithID: case VerificationStatus.comparingWithID:
@ -765,7 +784,7 @@ class SelfieVerificationStep extends StatelessWidget {
isLoading: true, isLoading: true,
title: 'Face Matching', title: 'Face Matching',
icon: Icons.compare, icon: Icons.compare,
customColor: Colors.blue, customColor: TColors.primary,
); );
case VerificationStatus.livenessCompleted: case VerificationStatus.livenessCompleted:
@ -870,10 +889,7 @@ class SelfieVerificationStep extends StatelessWidget {
'Ensure your entire face is visible', 'Ensure your entire face is visible',
'Avoid shadows on your face', 'Avoid shadows on your face',
], ],
backgroundColor: TColors.primary.withOpacity(0.1),
textColor: TColors.primary,
iconColor: TColors.primary,
borderColor: TColors.primary.withOpacity(0.3),
); );
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:logger/web.dart';
import 'package:sigap/src/cores/services/supabase_service.dart'; import 'package:sigap/src/cores/services/supabase_service.dart';
import 'package:sigap/src/features/daily-ops/data/models/models/units_model.dart'; import 'package:sigap/src/features/daily-ops/data/models/models/units_model.dart';
import 'package:sigap/src/utils/exceptions/exceptions.dart'; import 'package:sigap/src/utils/exceptions/exceptions.dart';
@ -12,10 +13,13 @@ class UnitRepository extends GetxController {
// Get all units // Get all units
Future<List<UnitModel>> getAllUnits() async { Future<List<UnitModel>> getAllUnits() async {
try { try {
final units = await _supabase final units = await _supabase.from('units').select().order('name');
.from('units')
.select() if (units.isEmpty) {
.order('name'); return [];
}
Logger().i('Fetched ${units.length} units from database');
return units.map((unit) => UnitModel.fromJson(unit)).toList(); return units.map((unit) => UnitModel.fromJson(unit)).toList();
} on PostgrestException catch (error) { } on PostgrestException catch (error) {
@ -28,11 +32,12 @@ class UnitRepository extends GetxController {
// Get unit by ID // Get unit by ID
Future<UnitModel> getUnitById(String codeUnit) async { Future<UnitModel> getUnitById(String codeUnit) async {
try { try {
final unit = await _supabase final unit =
.from('units') await _supabase
.select('*, officers(*), patrol_units(*), unit_statistics(*)') .from('units')
.eq('code_unit', codeUnit) .select('*, officers(*), patrol_units(*), unit_statistics(*)')
.single(); .eq('code_unit', codeUnit)
.single();
return UnitModel.fromJson(unit); return UnitModel.fromJson(unit);
} on PostgrestException catch (error) { } on PostgrestException catch (error) {
@ -77,15 +82,17 @@ class UnitRepository extends GetxController {
} }
// Get units near a location // Get units near a location
Future<List<UnitModel>> getUnitsNearLocation(double latitude, double longitude, double radiusInKm) async { Future<List<UnitModel>> getUnitsNearLocation(
double latitude,
double longitude,
double radiusInKm,
) async {
try { try {
// Use PostGIS to find units within a radius // Use PostGIS to find units within a radius
final units = await _supabase final units = await _supabase.rpc(
.rpc('get_units_near_location', params: { 'get_units_near_location',
'lat': latitude, params: {'lat': latitude, 'lng': longitude, 'radius_km': radiusInKm},
'lng': longitude, );
'radius_km': radiusInKm,
});
return units.map((unit) => UnitModel.fromJson(unit)).toList(); return units.map((unit) => UnitModel.fromJson(unit)).toList();
} on PostgrestException catch (error) { } on PostgrestException catch (error) {

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.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';
import 'package:sigap/src/utils/helpers/helper_functions.dart';
class ImageSourceDialog { class ImageSourceDialog {
static Future<void> show({ static Future<void> show({
@ -16,62 +17,69 @@ class ImageSourceDialog {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd), borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
), ),
child: Padding( child: Builder(
padding: const EdgeInsets.all(TSizes.defaultSpace), builder:
child: Column( (context) => Padding(
mainAxisSize: MainAxisSize.min, padding: const EdgeInsets.all(TSizes.defaultSpace),
children: [ child: Column(
Text( mainAxisSize: MainAxisSize.min,
title, children: [
style: TextStyle( Text(
fontSize: TSizes.fontSizeLg, title,
fontWeight: FontWeight.bold, style: TextStyle(
), fontSize: TSizes.fontSizeLg,
), fontWeight: FontWeight.bold,
const SizedBox(height: TSizes.md), ),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: TSizes.fontSizeSm,
color: TColors.textSecondary,
),
),
const SizedBox(height: TSizes.spaceBtwItems),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildImageSourceOption(
icon: Icons.camera_alt,
label: 'Camera',
onTap: () {
onSourceSelected(ImageSource.camera);
Get.back();
},
),
if (galleryOption)
_buildImageSourceOption(
icon: Icons.image,
label: 'Gallery',
onTap: () {
onSourceSelected(ImageSource.gallery);
Get.back();
},
), ),
], const SizedBox(height: TSizes.md),
Text(
message,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: TSizes.fontSizeSm,
color: TColors.textSecondary,
),
),
const SizedBox(height: TSizes.spaceBtwItems),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildImageSourceOption(
context: context,
icon: Icons.camera_alt,
label: 'Camera',
onTap: () {
onSourceSelected(ImageSource.camera);
Get.back();
},
),
if (galleryOption)
_buildImageSourceOption(
context: context,
icon: Icons.image,
label: 'Gallery',
onTap: () {
onSourceSelected(ImageSource.gallery);
Get.back();
},
),
],
),
],
),
), ),
],
),
), ),
), ),
); );
} }
static Widget _buildImageSourceOption({ static Widget _buildImageSourceOption({
required BuildContext context,
required IconData icon, required IconData icon,
required String label, required String label,
required VoidCallback onTap, required VoidCallback onTap,
}) { }) {
final isDark = THelperFunctions.isDarkMode(context);
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Column( child: Column(
@ -79,13 +87,25 @@ class ImageSourceDialog {
Container( Container(
padding: const EdgeInsets.all(TSizes.md), padding: const EdgeInsets.all(TSizes.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: TColors.primary.withOpacity(0.1), color:
isDark
? TColors.cardDark.withOpacity(0.7)
: TColors.primary.withOpacity(0.1),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(icon, color: TColors.primary, size: TSizes.iconLg), child: Icon(
icon,
color: isDark ? TColors.cardForegroundDark : TColors.primary,
size: TSizes.iconLg,
),
), ),
const SizedBox(height: TSizes.sm), const SizedBox(height: TSizes.sm),
Text(label), Text(
label,
style: TextStyle(
color: isDark ? TColors.cardForegroundDark : TColors.primary,
),
),
], ],
), ),
); );

View File

@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.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';
import 'package:sigap/src/utils/loaders/circular_loader.dart';
class ImageUploader extends StatelessWidget { class ImageUploader extends StatelessWidget {
final XFile? image; final XFile? image;
@ -121,7 +122,7 @@ class ImageUploader extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const CircularProgressIndicator(), const TCircularLoader(),
const SizedBox(height: TSizes.sm), const SizedBox(height: TSizes.sm),
Text( Text(
'Uploading...', 'Uploading...',

View File

@ -180,7 +180,7 @@ class StandardStepIndicator extends StatelessWidget {
isActive isActive
? Icon( ? Icon(
index < currentStep ? Icons.check : Icons.circle, index < currentStep ? Icons.check : Icons.circle,
color: Colors.white, color: isDark ? TColors.primary : TColors.light,
size: index < currentStep ? TSizes.iconSm : TSizes.iconXs, size: index < currentStep ? TSizes.iconSm : TSizes.iconXs,
) )
: Text( : Text(

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sigap/src/utils/constants/colors.dart';
import 'package:sigap/src/utils/constants/sizes.dart'; import 'package:sigap/src/utils/constants/sizes.dart';
class TipsContainer extends StatelessWidget { class TipsContainer extends StatelessWidget {
@ -14,10 +15,10 @@ class TipsContainer extends StatelessWidget {
super.key, super.key,
required this.title, required this.title,
required this.tips, required this.tips,
this.backgroundColor = Colors.blue, this.backgroundColor = TColors.cardInformation,
this.borderColor = Colors.blue, this.borderColor = TColors.cardInformation,
this.textColor = Colors.blue, this.textColor = TColors.cardInformation,
this.iconColor = Colors.blue, this.iconColor = TColors.cardInformation,
this.leadingIcon = Icons.tips_and_updates, this.leadingIcon = Icons.tips_and_updates,
}); });

View File

@ -43,6 +43,40 @@ class TColors {
static const Color lightGrey = Color(0xFFF9F9F9); static const Color lightGrey = Color(0xFFF9F9F9);
static const Color white = Color(0xFFFFFFFF); static const Color white = Color(0xFFFFFFFF);
// Card colors
static const Color cardPrimary = Color(0xFF232425); // Dark card bg
static const Color cardSecondary = Color(0xFFFFFFFF); // Light card bg
static const Color cardForeground = Color(0xFF2F2F2F); // Card text color
static const Color cardMuted = Color(0xFF343536); // Muted card text
static const Color cardMutedForeground = Color(
0xFFB0B0B0,
); // Muted text color
static const Color cardAccent = Color(0xFF343536); // Card accent color
static const Color cardDestructive = Color(
0xFFEF4444,
); // Card destructive action
static const Color cardSuccess = Color(0xFF38B2AC); // Card success action
static const Color cardWarning = Color(0xFFF59E0B); // Card warning action
static const Color cardInformation = Color(
0xFF2563EB,
); // Card information action
// Card colors (theme-aware)
static const Color cardLight = Color(0xFFFFFFFF); // Light mode card bg
static const Color cardDark = Color(0xFF232425); // Dark mode card bg
static const Color cardBorderLight = Color(
0xFFE5E5E5,
); // Light mode card border
static const Color cardBorderDark = Color(
0xFF343536,
); // Dark mode card border
static const Color cardForegroundLight = Color(
0xFF2F2F2F,
); // Light mode card text
static const Color cardForegroundDark = Color(
0xFFFFFFFF,
); // Dark mode card text
// Additional colors // Additional colors
static const Color transparent = Colors.transparent; static const Color transparent = Colors.transparent;

View File

@ -2,5 +2,5 @@ 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 = 5; static const int totalStepOfficer = 4;
} }

View File

@ -1,4 +1,4 @@
/* /*
Warnings: Warnings:
- You are about to drop the `test` table. If the table is not empty, all the data it contains will be lost. - You are about to drop the `test` table. If the table is not empty, all the data it contains will be lost.