feat: Enhance Patrol Unit Management
- Added new fields `category` and `memberCount` to `PatrolUnitModel` for better categorization and tracking of patrol units. - Introduced `PatrolUnitController` to manage patrol unit operations, including creation, validation, and fetching of patrol units. - Implemented enhanced validation for patrol unit creation, ensuring all required fields are correctly filled. - Updated `updateOfficerProfile` method in `OfficerRepository` to return nullable `OfficerModel`. - Improved `CustomTextField` widget to handle disabled states with appropriate styling. - Added new colors for information and disabled states in `TColors`. - Refactored `UserRepository` to update user metadata with role information using a new `UserMetadataModel`.
This commit is contained in:
parent
105d992faa
commit
407233916b
|
@ -85,11 +85,6 @@ class AuthenticationRepository extends GetxController {
|
||||||
final isProfileComplete =
|
final isProfileComplete =
|
||||||
session?.user.userMetadata?['profile_status'] == 'completed';
|
session?.user.userMetadata?['profile_status'] == 'completed';
|
||||||
|
|
||||||
// Log the current state for debugging
|
|
||||||
Logger().d(
|
|
||||||
'Screen redirect state - Session: ${session != null}, Email verified: $isEmailVerified, Profile complete: $isProfileComplete, First time: $isFirstTime, Current route: ${Get.currentRoute}',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cek lokasi terlebih dahulu
|
// Cek lokasi terlebih dahulu
|
||||||
if (await _locationService.isLocationValidForFeature() == false) {
|
if (await _locationService.isLocationValidForFeature() == false) {
|
||||||
_navigateToRoute(AppRoutes.locationWarning);
|
_navigateToRoute(AppRoutes.locationWarning);
|
||||||
|
@ -674,7 +669,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
// Don't attempt profile completion while already redirecting
|
// Don't attempt profile completion while already redirecting
|
||||||
throw 'Cannot complete profile during redirection. Please try again.';
|
throw 'Cannot complete profile during redirection. Please try again.';
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Convert to UserModel
|
// Convert to UserModel
|
||||||
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
|
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
|
||||||
|
@ -697,7 +692,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
.from('profiles')
|
.from('profiles')
|
||||||
.insert(completeData.viewerData!.toJson());
|
.insert(completeData.viewerData!.toJson());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set redirection flag to ensure we don't navigate before setup is complete
|
// Set redirection flag to ensure we don't navigate before setup is complete
|
||||||
_isRedirecting = true;
|
_isRedirecting = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,94 +1,67 @@
|
||||||
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:logger/logger.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/patrol_units_model.dart';
|
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/units_model.dart';
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/repositories/patrol_units_repository.dart';
|
import 'package:sigap/src/features/daily-ops/data/models/index.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/repositories/units_repository.dart';
|
import 'package:sigap/src/features/daily-ops/data/repositories/units_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/officers_repository.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/repositories/users_repository.dart';
|
||||||
|
import 'package:sigap/src/shared/widgets/state_screeen/state_screen.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
|
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||||
|
import 'package:sigap/src/utils/popups/circular_full_screen_loader.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.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();
|
||||||
|
|
||||||
// Static form key
|
|
||||||
// final GlobalKey<FormState> formKey = TGlobalFormKey.officerInfo();
|
|
||||||
|
|
||||||
final RxBool isFormValid = RxBool(true);
|
final RxBool isFormValid = RxBool(true);
|
||||||
|
|
||||||
// Data states
|
// Data states
|
||||||
final RxList<UnitModel> availableUnits = <UnitModel>[].obs;
|
final RxList<UnitModel> availableUnits = <UnitModel>[].obs;
|
||||||
final RxList<PatrolUnitModel> availablePatrolUnits = <PatrolUnitModel>[].obs;
|
|
||||||
final RxBool isLoadingUnits = false.obs;
|
final RxBool isLoadingUnits = false.obs;
|
||||||
final RxBool isLoadingPatrolUnits = false.obs;
|
|
||||||
final RxString selectedUnitName = ''.obs;
|
final RxString selectedUnitName = ''.obs;
|
||||||
final RxString selectedPatrolUnitName = ''.obs;
|
|
||||||
|
|
||||||
// Additional data states for patrol unit configuration
|
// Controllers for officer info
|
||||||
final Rx<PatrolUnitType> selectedPatrolType = PatrolUnitType.car.obs;
|
|
||||||
final Rx<PatrolSelectionMode> patrolSelectionMode =
|
|
||||||
PatrolSelectionMode.individual.obs;
|
|
||||||
final RxBool isCreatingNewPatrolUnit = false.obs;
|
|
||||||
final RxString newPatrolUnitName = ''.obs;
|
|
||||||
|
|
||||||
// Controllers
|
|
||||||
final nrpController = TextEditingController();
|
final nrpController = TextEditingController();
|
||||||
final rankController = TextEditingController();
|
|
||||||
final unitIdController = TextEditingController();
|
|
||||||
final patrolUnitIdController = TextEditingController();
|
|
||||||
final nameController = TextEditingController();
|
final nameController = TextEditingController();
|
||||||
|
final rankController = TextEditingController();
|
||||||
final positionController = TextEditingController();
|
final positionController = TextEditingController();
|
||||||
final phoneController = TextEditingController();
|
final phoneController = TextEditingController();
|
||||||
final emailController = TextEditingController();
|
final unitIdController = TextEditingController();
|
||||||
final validUntilController = TextEditingController();
|
final validUntilController = TextEditingController();
|
||||||
final avatarController = TextEditingController();
|
|
||||||
final qrCodeController = TextEditingController();
|
|
||||||
final bannedReasonController = TextEditingController();
|
|
||||||
final bannedUntilController = TextEditingController();
|
|
||||||
|
|
||||||
// Controllers for new patrol unit
|
// New fields based on the model
|
||||||
final patrolNameController = TextEditingController();
|
final placeOfBirthController = TextEditingController();
|
||||||
final patrolTypeController = TextEditingController();
|
final dateOfBirthController = TextEditingController();
|
||||||
final patrolRadiusController = TextEditingController(
|
|
||||||
text: '500',
|
|
||||||
); // Default radius in meters
|
|
||||||
|
|
||||||
// Error states
|
// Error states
|
||||||
final RxString nrpError = ''.obs;
|
final RxString nrpError = ''.obs;
|
||||||
|
final RxString nameError = ''.obs;
|
||||||
final RxString rankError = ''.obs;
|
final RxString rankError = ''.obs;
|
||||||
final RxString unitIdError = ''.obs;
|
final RxString unitIdError = ''.obs;
|
||||||
final RxString patrolUnitIdError = ''.obs;
|
|
||||||
final RxString nameError = ''.obs;
|
|
||||||
final RxString positionError = ''.obs;
|
final RxString positionError = ''.obs;
|
||||||
final RxString phoneError = ''.obs;
|
final RxString phoneError = ''.obs;
|
||||||
final RxString emailError = ''.obs;
|
|
||||||
final RxString validUntilError = ''.obs;
|
final RxString validUntilError = ''.obs;
|
||||||
final RxString avatarError = ''.obs;
|
final RxString placeOfBirthError = ''.obs;
|
||||||
final RxString qrCodeError = ''.obs;
|
final RxString dateOfBirthError = ''.obs;
|
||||||
final RxString bannedReasonError = ''.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
|
// Logger instance
|
||||||
final Logger logger = Logger();
|
final Logger logger = Logger();
|
||||||
|
|
||||||
// Make sure repositories are properly initialized
|
// Unit repository
|
||||||
late final UnitRepository unitRepository;
|
late final UnitRepository unitRepository;
|
||||||
late final PatrolUnitRepository patrolUnitRepository;
|
|
||||||
|
|
||||||
// Dropdown open state
|
// Dropdown open state
|
||||||
RxBool isUnitDropdownOpen = false.obs;
|
RxBool isUnitDropdownOpen = false.obs;
|
||||||
|
|
||||||
|
// Date selection related
|
||||||
|
Rx<DateTime?> selectedValidUntil = Rx<DateTime?>(null);
|
||||||
|
Rx<DateTime?> selectedDateOfBirth = Rx<DateTime?>(null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
@ -102,9 +75,7 @@ class OfficerInfoController extends GetxController {
|
||||||
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>();
|
||||||
patrolUnitRepository = Get.find<PatrolUnitRepository>();
|
logger.i('UnitRepository initialized');
|
||||||
|
|
||||||
Logger().i('UnitRepository and PatrolUnitRepository initialized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch available units with improved error handling
|
// Fetch available units with improved error handling
|
||||||
|
@ -139,377 +110,206 @@ class OfficerInfoController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Handle unit selection
|
||||||
void onUnitSelected(UnitModel unit) {
|
void onUnitSelected(UnitModel unit) {
|
||||||
unitIdController.text = unit.codeUnit;
|
unitIdController.text = unit.codeUnit;
|
||||||
selectedUnitName.value = unit.name;
|
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
|
// Set valid until date
|
||||||
void onPatrolUnitSelected(PatrolUnitModel patrolUnit) {
|
void setValidUntilDate(DateTime date) {
|
||||||
patrolUnitIdController.text = patrolUnit.id;
|
selectedValidUntil.value = date;
|
||||||
selectedPatrolUnitName.value = patrolUnit.name;
|
validUntilController.text = formatDate(date);
|
||||||
|
validUntilError.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set patrol unit type (car or motorcycle)
|
// Set date of birth
|
||||||
void setPatrolUnitType(PatrolUnitType type) {
|
void setDateOfBirth(DateTime date) {
|
||||||
selectedPatrolType.value = type;
|
selectedDateOfBirth.value = date;
|
||||||
patrolTypeController.text = type.name;
|
dateOfBirthController.text = formatDate(date);
|
||||||
|
dateOfBirthError.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set patrol selection mode (individual, group, or create new)
|
// Format date as yyyy-MM-dd
|
||||||
void setPatrolSelectionMode(PatrolSelectionMode mode) {
|
String formatDate(DateTime date) {
|
||||||
patrolSelectionMode.value = mode;
|
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
// Reset fields when changing modes
|
// Populate fields from KTA data
|
||||||
if (mode == PatrolSelectionMode.createNew) {
|
void populateFromKta(KtaModel ktaData) {
|
||||||
isCreatingNewPatrolUnit.value = true;
|
if (ktaData.nrp.isNotEmpty && nrpController.text.isEmpty) {
|
||||||
patrolUnitIdController.clear();
|
nrpController.text = ktaData.nrp;
|
||||||
selectedPatrolUnitName.value = '';
|
}
|
||||||
} else {
|
|
||||||
isCreatingNewPatrolUnit.value = false;
|
if (ktaData.name.isNotEmpty && nameController.text.isEmpty) {
|
||||||
|
nameController.text = ktaData.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ktaData.rank?.isNotEmpty == true && rankController.text.isEmpty) {
|
||||||
|
rankController.text = ktaData.rank!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ktaData.position?.isNotEmpty == true &&
|
||||||
|
positionController.text.isEmpty) {
|
||||||
|
positionController.text = ktaData.position!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate new patrol unit data
|
// Validate the form
|
||||||
bool validateNewPatrolUnit() {
|
bool validate(GlobalKey<FormState>? formKey) {
|
||||||
bool isValid = true;
|
clearErrors();
|
||||||
|
|
||||||
// Clear previous errors
|
bool isValid = formKey?.currentState?.validate() ?? false;
|
||||||
patrolNameError.value = '';
|
|
||||||
patrolTypeError.value = '';
|
|
||||||
patrolRadiusError.value = '';
|
|
||||||
|
|
||||||
// Validate patrol unit name
|
// Additional validation for required fields
|
||||||
final nameValidation = TValidators.validateUserInput(
|
if (nrpController.text.isEmpty) {
|
||||||
'Patrol Unit Name',
|
nrpError.value = 'NRP is required';
|
||||||
patrolNameController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (nameValidation != null) {
|
|
||||||
patrolNameError.value = nameValidation;
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate patrol unit type
|
if (nameController.text.isEmpty) {
|
||||||
if (patrolTypeController.text.isEmpty) {
|
nameError.value = 'Name is required';
|
||||||
patrolTypeError.value = 'Patrol type is required';
|
|
||||||
isValid = false;
|
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) {
|
if (unitIdController.text.isEmpty) {
|
||||||
TLoaders.errorSnackBar(
|
unitIdError.value = 'Please select a unit';
|
||||||
title: 'Unit Required',
|
isValid = false;
|
||||||
message: 'Please select a unit before creating a patrol unit',
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFormValid.value = isValid;
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateOfficer(OfficerModel officer, UserMetadataModel metadata) async {
|
||||||
try {
|
try {
|
||||||
// This would typically involve an API call to create the patrol unit
|
TCircularFullScreenLoader.openLoadingDialog();
|
||||||
// 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 isConnected = await NetworkManager.instance.isConnected();
|
||||||
/*
|
if (!isConnected) {
|
||||||
final newPatrolUnit = await patrolUnitRepository.createPatrolUnit(
|
TLoaders.errorSnackBar(
|
||||||
PatrolUnitModel(
|
title: 'No Internet Connection',
|
||||||
id: '', // Will be generated by the backend
|
message: 'Please check your internet connection and try again.',
|
||||||
unitId: unitIdController.text,
|
);
|
||||||
locationId: '', // This might need to be set elsewhere
|
TCircularFullScreenLoader.stopLoading();
|
||||||
name: patrolNameController.text,
|
return;
|
||||||
type: selectedPatrolType.value.name,
|
}
|
||||||
status: 'active', // Default status
|
|
||||||
radius: double.parse(patrolRadiusController.text),
|
// Validate the form before proceeding
|
||||||
createdAt: DateTime.now(),
|
if (!validate(null)) {
|
||||||
),
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Validation Error',
|
||||||
|
message: 'Please fix the errors in the form before submitting.',
|
||||||
|
);
|
||||||
|
TCircularFullScreenLoader.stopLoading();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = officer.copyWith(
|
||||||
|
nrp: nrpController.text,
|
||||||
|
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()}');
|
||||||
|
|
||||||
|
|
||||||
// If successful, set the patrol unit ID and name
|
|
||||||
patrolUnitIdController.text = newPatrolUnit.id;
|
|
||||||
selectedPatrolUnitName.value = newPatrolUnit.name;
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Simulate success
|
// final updatedOfficer = await OfficerRepository.instance.updateOfficer(
|
||||||
TLoaders.successSnackBar(
|
// data,
|
||||||
title: 'Success',
|
// );
|
||||||
message: 'Patrol unit created successfully',
|
|
||||||
);
|
// if (updatedOfficer == null) {
|
||||||
return true;
|
// TLoaders.errorSnackBar(
|
||||||
} catch (error) {
|
// title: 'Update Failed',
|
||||||
|
// message: 'Failed to update officer information. Please try again.',
|
||||||
|
// );
|
||||||
|
// TCircularFullScreenLoader.stopLoading();
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// final userMetadata =
|
||||||
|
// metadata.copyWith(profileStatus: 'completed').toAuthMetadataJson();
|
||||||
|
|
||||||
|
// await UserRepository.instance.updateUserMetadata(userMetadata);
|
||||||
|
|
||||||
|
// TLoaders.successSnackBar(
|
||||||
|
// title: 'Update Successful',
|
||||||
|
// message: 'Officer information updated successfully.',
|
||||||
|
// );
|
||||||
|
|
||||||
|
// resetForm();
|
||||||
|
// TCircularFullScreenLoader.stopLoading();
|
||||||
|
|
||||||
|
// Get.off(
|
||||||
|
// () => StateScreen(
|
||||||
|
// title: 'Officer Information Created',
|
||||||
|
// subtitle: 'Officer information has been successfully create.',
|
||||||
|
// primaryButtonTitle: 'Back to signin',
|
||||||
|
// image: TImages.womanHuggingEarth,
|
||||||
|
// showButton: true,
|
||||||
|
// onPressed: () => AuthenticationRepository.instance.screenRedirect(),
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
} catch (e) {
|
||||||
|
logger.e('Error updating officer: $e');
|
||||||
|
TCircularFullScreenLoader.stopLoading();
|
||||||
TLoaders.errorSnackBar(
|
TLoaders.errorSnackBar(
|
||||||
title: 'Error',
|
title: 'Update Failed',
|
||||||
message: 'Failed to create patrol unit: $error',
|
message: 'An error occurred while updating officer information.',
|
||||||
);
|
);
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join an existing patrol unit
|
void resetForm() {
|
||||||
void joinPatrolUnit(PatrolUnitModel patrolUnit) {
|
nrpController.clear();
|
||||||
patrolUnitIdController.text = patrolUnit.id;
|
nameController.clear();
|
||||||
selectedPatrolUnitName.value = patrolUnit.name;
|
rankController.clear();
|
||||||
|
positionController.clear();
|
||||||
|
phoneController.clear();
|
||||||
|
unitIdController.clear();
|
||||||
|
validUntilController.clear();
|
||||||
|
placeOfBirthController.clear();
|
||||||
|
dateOfBirthController.clear();
|
||||||
|
|
||||||
// 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) {
|
|
||||||
clearErrors();
|
clearErrors();
|
||||||
|
|
||||||
if (formKey.currentState?.validate() ?? false) {
|
selectedUnitName.value = '';
|
||||||
return true;
|
selectedValidUntil.value = null;
|
||||||
}
|
selectedDateOfBirth.value = null;
|
||||||
|
|
||||||
final nrpValidation = TValidators.validateUserInput(
|
|
||||||
'NRP',
|
|
||||||
nrpController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (nrpValidation != null) {
|
|
||||||
nrpError.value = nrpValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final rankValidation = TValidators.validateUserInput(
|
|
||||||
'Rank',
|
|
||||||
rankController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (rankValidation != null) {
|
|
||||||
rankError.value = rankValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final unitIdValidation = TValidators.validateUserInput(
|
|
||||||
'Unit ID',
|
|
||||||
unitIdController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (unitIdValidation != null) {
|
|
||||||
unitIdError.value = unitIdValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final patrolUnitIdValidation = TValidators.validateUserInput(
|
|
||||||
'Patrol Unit ID',
|
|
||||||
patrolUnitIdController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (patrolUnitIdValidation != null) {
|
|
||||||
patrolUnitIdError.value = patrolUnitIdValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final nameValidation = TValidators.validateUserInput(
|
|
||||||
'Name',
|
|
||||||
nameController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (nameValidation != null) {
|
|
||||||
nameError.value = nameValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final positionValidation = TValidators.validateUserInput(
|
|
||||||
'Position',
|
|
||||||
positionController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (positionValidation != null) {
|
|
||||||
positionError.value = positionValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final phoneValidation = TValidators.validateUserInput(
|
|
||||||
'Phone',
|
|
||||||
phoneController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (phoneValidation != null) {
|
|
||||||
phoneError.value = phoneValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final emailValidation = TValidators.validateUserInput(
|
|
||||||
'Email',
|
|
||||||
emailController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (emailValidation != null) {
|
|
||||||
emailError.value = emailValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final validUntilValidation = TValidators.validateUserInput(
|
|
||||||
'Valid Until',
|
|
||||||
validUntilController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (validUntilValidation != null) {
|
|
||||||
validUntilError.value = validUntilValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final avatarValidation = TValidators.validateUserInput(
|
|
||||||
'Avatar',
|
|
||||||
avatarController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (avatarValidation != null) {
|
|
||||||
avatarError.value = avatarValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final qrCodeValidation = TValidators.validateUserInput(
|
|
||||||
'QR Code',
|
|
||||||
qrCodeController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (qrCodeValidation != null) {
|
|
||||||
qrCodeError.value = qrCodeValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bannedReasonValidation = TValidators.validateUserInput(
|
|
||||||
'Banned Reason',
|
|
||||||
bannedReasonController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (bannedReasonValidation != null) {
|
|
||||||
bannedReasonError.value = bannedReasonValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final bannedUntilValidation = TValidators.validateUserInput(
|
|
||||||
'Banned Until',
|
|
||||||
bannedUntilController.text,
|
|
||||||
50,
|
|
||||||
);
|
|
||||||
if (bannedUntilValidation != null) {
|
|
||||||
bannedUntilError.value = bannedUntilValidation;
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include validation for new patrol unit if creating one
|
|
||||||
if (isCreatingNewPatrolUnit.value && !validateNewPatrolUnit()) {
|
|
||||||
isFormValid.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isFormValid.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear all error messages
|
||||||
void clearErrors() {
|
void clearErrors() {
|
||||||
nrpError.value = '';
|
nrpError.value = '';
|
||||||
|
nameError.value = '';
|
||||||
rankError.value = '';
|
rankError.value = '';
|
||||||
unitIdError.value = '';
|
unitIdError.value = '';
|
||||||
patrolUnitIdError.value = '';
|
|
||||||
nameError.value = '';
|
|
||||||
positionError.value = '';
|
positionError.value = '';
|
||||||
phoneError.value = '';
|
phoneError.value = '';
|
||||||
emailError.value = '';
|
|
||||||
validUntilError.value = '';
|
validUntilError.value = '';
|
||||||
avatarError.value = '';
|
placeOfBirthError.value = '';
|
||||||
qrCodeError.value = '';
|
dateOfBirthError.value = '';
|
||||||
bannedReasonError.value = '';
|
|
||||||
bannedUntilError.value = '';
|
|
||||||
|
|
||||||
// Clear errors for new patrol unit fields
|
|
||||||
patrolNameError.value = '';
|
|
||||||
patrolTypeError.value = '';
|
|
||||||
patrolRadiusError.value = '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up resources
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
nrpController.dispose();
|
nrpController.dispose();
|
||||||
rankController.dispose();
|
|
||||||
unitIdController.dispose();
|
|
||||||
patrolUnitIdController.dispose();
|
|
||||||
nameController.dispose();
|
nameController.dispose();
|
||||||
|
rankController.dispose();
|
||||||
positionController.dispose();
|
positionController.dispose();
|
||||||
phoneController.dispose();
|
phoneController.dispose();
|
||||||
emailController.dispose();
|
unitIdController.dispose();
|
||||||
validUntilController.dispose();
|
validUntilController.dispose();
|
||||||
avatarController.dispose();
|
placeOfBirthController.dispose();
|
||||||
qrCodeController.dispose();
|
dateOfBirthController.dispose();
|
||||||
bannedReasonController.dispose();
|
|
||||||
bannedUntilController.dispose();
|
|
||||||
|
|
||||||
// Dispose controllers for new patrol unit
|
|
||||||
patrolNameController.dispose();
|
|
||||||
patrolTypeController.dispose();
|
|
||||||
patrolRadiusController.dispose();
|
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/main/registration_form_controller.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/id-card-verification/id_card_verification_controller.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart';
|
||||||
import 'package:sigap/src/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/colors.dart';
|
||||||
|
@ -19,6 +20,12 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
final mainController = Get.find<FormRegistrationController>();
|
final mainController = Get.find<FormRegistrationController>();
|
||||||
mainController.formKey = formKey;
|
mainController.formKey = formKey;
|
||||||
|
|
||||||
|
// Check if KTA data exists and populate fields if available
|
||||||
|
_populateFieldsFromKta(
|
||||||
|
controller,
|
||||||
|
mainController.idCardVerificationController,
|
||||||
|
);
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
key: formKey,
|
key: formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
@ -34,6 +41,7 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
// NRP field
|
// NRP field
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
label: 'NRP',
|
label: 'NRP',
|
||||||
|
enabled: controller.nrpController.text.isEmpty,
|
||||||
controller: controller.nrpController,
|
controller: controller.nrpController,
|
||||||
validator: TValidators.validateNRP,
|
validator: TValidators.validateNRP,
|
||||||
errorText: controller.nrpError.value,
|
errorText: controller.nrpError.value,
|
||||||
|
@ -45,24 +53,34 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.nrpError.value = '';
|
controller.nrpError.value = '';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Obx(
|
_buildErrorText(controller.nrpError),
|
||||||
() =>
|
|
||||||
controller.nrpError.value.isNotEmpty
|
// Name field
|
||||||
? Padding(
|
CustomTextField(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
label: 'Full Name',
|
||||||
child: Text(
|
controller: controller.nameController,
|
||||||
controller.nrpError.value,
|
validator:
|
||||||
style: TextStyle(color: Colors.red[700], fontSize: 12),
|
(v) => TValidators.validateUserInput(
|
||||||
),
|
'Name',
|
||||||
)
|
v,
|
||||||
: const SizedBox.shrink(),
|
100,
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
|
errorText: controller.nameError.value,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
hintText: 'Your full name',
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.nameController.text = value;
|
||||||
|
controller.nameError.value = '';
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
_buildErrorText(controller.nameError),
|
||||||
|
|
||||||
// Rank field
|
// Rank field
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
label: 'Rank',
|
label: 'Rank',
|
||||||
controller: controller.rankController,
|
controller: controller.rankController,
|
||||||
validator: TValidators.validateRank,
|
validator: (v) => TValidators.validateUserInput('Rank', v, 100),
|
||||||
errorText: controller.rankError.value,
|
errorText: controller.rankError.value,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
hintText: 'e.g., Captain',
|
hintText: 'e.g., Captain',
|
||||||
|
@ -71,30 +89,13 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.rankError.value = '';
|
controller.rankError.value = '';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Obx(
|
_buildErrorText(controller.rankError),
|
||||||
() =>
|
|
||||||
controller.rankError.value.isNotEmpty
|
|
||||||
? Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
|
||||||
child: Text(
|
|
||||||
controller.rankError.value,
|
|
||||||
style: TextStyle(color: Colors.red[700], fontSize: 12),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Position field
|
// Position field
|
||||||
CustomTextField(
|
CustomTextField(
|
||||||
label: 'Position',
|
label: 'Position',
|
||||||
controller: controller.positionController,
|
controller: controller.positionController,
|
||||||
validator:
|
validator: (v) => TValidators.validateUserInput('Position', v, 100),
|
||||||
(v) => TValidators.validateUserInput(
|
|
||||||
'Position',
|
|
||||||
v,
|
|
||||||
100,
|
|
||||||
required: true,
|
|
||||||
),
|
|
||||||
errorText: controller.positionError.value,
|
errorText: controller.positionError.value,
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
hintText: 'e.g., Head of Unit',
|
hintText: 'e.g., Head of Unit',
|
||||||
|
@ -103,18 +104,73 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller.positionError.value = '';
|
controller.positionError.value = '';
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Obx(
|
_buildErrorText(controller.positionError),
|
||||||
() =>
|
|
||||||
controller.positionError.value.isNotEmpty
|
// Phone field
|
||||||
? Padding(
|
CustomTextField(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
label: 'Phone Number',
|
||||||
child: Text(
|
controller: controller.phoneController,
|
||||||
controller.positionError.value,
|
validator: TValidators.validatePhoneNumber,
|
||||||
style: TextStyle(color: Colors.red[700], fontSize: 12),
|
errorText: controller.phoneError.value,
|
||||||
),
|
textInputAction: TextInputAction.next,
|
||||||
)
|
keyboardType: TextInputType.phone,
|
||||||
: const SizedBox.shrink(),
|
hintText: 'e.g., 08123456789',
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.phoneController.text = value;
|
||||||
|
controller.phoneError.value = '';
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
_buildErrorText(controller.phoneError),
|
||||||
|
|
||||||
|
// Place of Birth field
|
||||||
|
CustomTextField(
|
||||||
|
label: 'Place of Birth',
|
||||||
|
controller: controller.placeOfBirthController,
|
||||||
|
validator:
|
||||||
|
(v) => TValidators.validateUserInput('Place of Birth', v, 100),
|
||||||
|
errorText: controller.placeOfBirthError.value,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
hintText: 'e.g., Jakarta',
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.placeOfBirthController.text = value;
|
||||||
|
controller.placeOfBirthError.value = '';
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildErrorText(controller.placeOfBirthError),
|
||||||
|
|
||||||
|
// Date of Birth field
|
||||||
|
_buildDateField(
|
||||||
|
context: context,
|
||||||
|
controller: controller,
|
||||||
|
label: 'Date of Birth',
|
||||||
|
textController: controller.dateOfBirthController,
|
||||||
|
errorValue: controller.dateOfBirthError,
|
||||||
|
hintText: 'YYYY-MM-DD',
|
||||||
|
initialDate: DateTime.now().subtract(
|
||||||
|
const Duration(days: 365 * 18),
|
||||||
|
), // Default to 18 years ago
|
||||||
|
firstDate: DateTime(1950),
|
||||||
|
lastDate: DateTime.now(),
|
||||||
|
onDateSelected: controller.setDateOfBirth,
|
||||||
|
),
|
||||||
|
_buildErrorText(controller.dateOfBirthError),
|
||||||
|
|
||||||
|
// Valid Until field
|
||||||
|
_buildDateField(
|
||||||
|
context: context,
|
||||||
|
controller: controller,
|
||||||
|
label: 'Valid Until',
|
||||||
|
textController: controller.validUntilController,
|
||||||
|
errorValue: controller.validUntilError,
|
||||||
|
hintText: 'YYYY-MM-DD',
|
||||||
|
initialDate: DateTime.now().add(
|
||||||
|
const Duration(days: 365),
|
||||||
|
), // Default to 1 year from now
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime(2100),
|
||||||
|
onDateSelected: controller.setValidUntilDate,
|
||||||
|
),
|
||||||
|
_buildErrorText(controller.validUntilError),
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwSections),
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
|
@ -127,154 +183,142 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
// Unit dropdown
|
// Unit dropdown
|
||||||
_buildUnitDropdown(controller),
|
_buildUnitDropdown(
|
||||||
|
controller,
|
||||||
|
mainController.idCardVerificationController,
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwSections),
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
// Patrol Unit Section - Simplified to show only current selection and a button to navigate
|
// Note about patrol unit assignment
|
||||||
const FormSectionHeader(
|
Container(
|
||||||
title: 'Patrol Unit',
|
padding: const EdgeInsets.all(12),
|
||||||
subtitle: 'Select or create your patrol unit',
|
decoration: BoxDecoration(
|
||||||
),
|
color: TColors.info.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
border: Border.all(color: TColors.info),
|
||||||
|
),
|
||||||
// Display selected patrol unit (if any)
|
child: Row(
|
||||||
Builder(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
builder: (context) {
|
children: [
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
Icon(Icons.info_outline, color: TColors.info),
|
||||||
final theme = Theme.of(context);
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
return GetX<OfficerInfoController>(
|
child: Column(
|
||||||
builder:
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
(controller) => Column(
|
children: [
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
Text(
|
||||||
children: [
|
'Patrol Unit Assignment',
|
||||||
if (controller.selectedPatrolUnitName.isNotEmpty)
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
Container(
|
fontWeight: FontWeight.bold,
|
||||||
padding: const EdgeInsets.all(12),
|
color: TColors.info,
|
||||||
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(
|
const SizedBox(height: 4),
|
||||||
() =>
|
Text(
|
||||||
controller.patrolUnitIdError.value.isNotEmpty
|
'After registration is complete, you will be able to join or create a patrol unit from the app.',
|
||||||
? Padding(
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
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
|
/// Populates form fields from KTA data if available
|
||||||
void _navigateToPatrolUnitSelectionScreen(OfficerInfoController controller) {
|
void _populateFieldsFromKta(
|
||||||
// Check if a unit is selected first
|
OfficerInfoController controller,
|
||||||
if (controller.unitIdController.text.isEmpty) {
|
IdCardVerificationController mainController,
|
||||||
Get.snackbar(
|
) {
|
||||||
'Unit Required',
|
// Check if KTA data exists in the main controller
|
||||||
'Please select a unit before configuring patrol unit',
|
final KtaModel? ktaData = mainController.ktaModel.value;
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.red.withOpacity(0.1),
|
|
||||||
colorText: Colors.red,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Get.to(() => PatrolUnitSelectionScreen());
|
if (ktaData != null) {
|
||||||
|
controller.populateFromKta(ktaData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to build error text consistently
|
||||||
|
Widget _buildErrorText(RxString errorValue) {
|
||||||
|
return Obx(
|
||||||
|
() =>
|
||||||
|
errorValue.value.isNotEmpty
|
||||||
|
? Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
errorValue.value,
|
||||||
|
style: TextStyle(color: Colors.red[700], fontSize: 12),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build date picker field
|
||||||
|
Widget _buildDateField({
|
||||||
|
required BuildContext context,
|
||||||
|
required OfficerInfoController controller,
|
||||||
|
required String label,
|
||||||
|
required TextEditingController textController,
|
||||||
|
required RxString errorValue,
|
||||||
|
required String hintText,
|
||||||
|
required DateTime initialDate,
|
||||||
|
required DateTime firstDate,
|
||||||
|
required DateTime lastDate,
|
||||||
|
required Function(DateTime) onDateSelected,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
CustomTextField(
|
||||||
|
label: label,
|
||||||
|
controller: textController,
|
||||||
|
readOnly: true, // Make read-only since we use date picker
|
||||||
|
errorText: errorValue.value,
|
||||||
|
hintText: hintText,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
icon: const Icon(Icons.calendar_today),
|
||||||
|
onPressed: () async {
|
||||||
|
final date = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: initialDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
builder: (context, child) {
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
colorScheme: ColorScheme.light(
|
||||||
|
primary: Theme.of(context).primaryColor,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
surface: Colors.white,
|
||||||
|
onSurface: Colors.black,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (date != null) {
|
||||||
|
onDateSelected(date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build unit dropdown selection
|
// Build unit dropdown selection
|
||||||
Widget _buildUnitDropdown(OfficerInfoController controller) {
|
Widget _buildUnitDropdown(
|
||||||
|
OfficerInfoController controller,
|
||||||
|
IdCardVerificationController idCardController,
|
||||||
|
) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -351,6 +395,34 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
(unit) => unit.codeUnit == controller.unitIdController.text,
|
(unit) => unit.codeUnit == controller.unitIdController.text,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If units are loaded and we have KTA data with a police unit,
|
||||||
|
// try to find a matching unit and select it
|
||||||
|
if (controller.availableUnits.isNotEmpty &&
|
||||||
|
controller.unitIdController.text.isEmpty) {
|
||||||
|
final ktaUnit =
|
||||||
|
idCardController.ktaModel.value?.policeUnit ?? '';
|
||||||
|
|
||||||
|
// More flexible matching logic to find the best matching unit
|
||||||
|
final matchingUnit = controller.availableUnits.firstWhereOrNull(
|
||||||
|
(unit) =>
|
||||||
|
// Try exact match first
|
||||||
|
unit.name.toLowerCase() == ktaUnit.toLowerCase() ||
|
||||||
|
// Then try contains match
|
||||||
|
unit.name.toLowerCase().contains(
|
||||||
|
ktaUnit.toLowerCase(),
|
||||||
|
) ||
|
||||||
|
// Or if the KTA unit contains the available unit name
|
||||||
|
ktaUnit.toLowerCase().contains(unit.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matchingUnit != null) {
|
||||||
|
// Use Future.microtask to avoid setState during build
|
||||||
|
Future.microtask(
|
||||||
|
() => controller.onUnitSelected(matchingUnit),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// Dropdown Selection Button
|
// Dropdown Selection Button
|
||||||
|
@ -580,18 +652,7 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
),
|
),
|
||||||
|
|
||||||
// Error message
|
// Error message
|
||||||
if (controller.unitIdError.value.isNotEmpty)
|
_buildErrorText(controller.unitIdError),
|
||||||
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),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,462 @@
|
||||||
|
import 'package:flutter/material.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/repositories/patrol_units_repository.dart';
|
||||||
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
|
// Define enums for patrol unit type and selection mode
|
||||||
|
enum PatrolUnitTypeExtended { car, motorcycle, foot, drone, mixed }
|
||||||
|
|
||||||
|
enum PatrolUnitStatus { active, standby, maintenance, patrol, onDuty, offDuty }
|
||||||
|
|
||||||
|
enum PatrolSelectionMode { individual, group, createNew }
|
||||||
|
|
||||||
|
class PatrolUnitController extends GetxController {
|
||||||
|
// Singleton instance
|
||||||
|
static PatrolUnitController get instance => Get.find();
|
||||||
|
|
||||||
|
// Data states
|
||||||
|
final RxList<PatrolUnitModel> availablePatrolUnits = <PatrolUnitModel>[].obs;
|
||||||
|
final RxBool isLoadingPatrolUnits = false.obs;
|
||||||
|
final RxString selectedPatrolUnitName = ''.obs;
|
||||||
|
final Rx<PatrolUnitTypeExtended> selectedPatrolType =
|
||||||
|
PatrolUnitTypeExtended.car.obs;
|
||||||
|
final Rx<PatrolSelectionMode> patrolSelectionMode =
|
||||||
|
PatrolSelectionMode.individual.obs;
|
||||||
|
|
||||||
|
// Controllers for selected patrol unit
|
||||||
|
final patrolUnitIdController = TextEditingController();
|
||||||
|
|
||||||
|
// Controllers for new patrol unit with enhanced fields
|
||||||
|
final patrolNameController = TextEditingController();
|
||||||
|
final patrolTypeController = TextEditingController(text: 'car');
|
||||||
|
final patrolRadiusController = TextEditingController(text: '6500');
|
||||||
|
final patrolStatusController = TextEditingController(text: 'active');
|
||||||
|
final memberCountController = TextEditingController(text: '1');
|
||||||
|
final categoryController = TextEditingController(text: 'individual');
|
||||||
|
|
||||||
|
// Unit ID for current selection
|
||||||
|
final unitIdController = TextEditingController();
|
||||||
|
final RxString selectedUnitName = ''.obs;
|
||||||
|
|
||||||
|
// Error states for patrol unit fields
|
||||||
|
final RxString patrolUnitIdError = ''.obs;
|
||||||
|
final RxString patrolNameError = ''.obs;
|
||||||
|
final RxString patrolTypeError = ''.obs;
|
||||||
|
final RxString patrolRadiusError = ''.obs;
|
||||||
|
final RxString patrolStatusError = ''.obs;
|
||||||
|
final RxString memberCountError = ''.obs;
|
||||||
|
final RxString categoryError = ''.obs;
|
||||||
|
|
||||||
|
// Logger instance
|
||||||
|
final Logger logger = Logger();
|
||||||
|
|
||||||
|
// Repository
|
||||||
|
late final PatrolUnitRepository patrolUnitRepository;
|
||||||
|
|
||||||
|
// Flag for creation mode
|
||||||
|
final RxBool isCreatingNewPatrolUnit = false.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
initRepository();
|
||||||
|
}
|
||||||
|
|
||||||
|
void initRepository() {
|
||||||
|
patrolUnitRepository = Get.find<PatrolUnitRepository>();
|
||||||
|
logger.i('PatrolUnitRepository initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch patrol units by unit ID
|
||||||
|
Future<void> getPatrolUnitsByUnitId(String unitId) async {
|
||||||
|
try {
|
||||||
|
isLoadingPatrolUnits.value = true;
|
||||||
|
availablePatrolUnits.clear();
|
||||||
|
|
||||||
|
final patrolUnits = await patrolUnitRepository.getPatrolUnitsByUnitId(
|
||||||
|
unitId,
|
||||||
|
);
|
||||||
|
availablePatrolUnits.value = patrolUnits;
|
||||||
|
|
||||||
|
logger.i(
|
||||||
|
'Fetched ${patrolUnits.length} patrol units for unit ID: $unitId',
|
||||||
|
);
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Reset patrol unit selection when switching between individual and group
|
||||||
|
patrolUnitIdController.clear();
|
||||||
|
selectedPatrolUnitName.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
update(['patrol_mode_tabs']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set patrol unit type
|
||||||
|
void setPatrolUnitType(PatrolUnitTypeExtended type) {
|
||||||
|
selectedPatrolType.value = type;
|
||||||
|
patrolTypeController.text = type.name;
|
||||||
|
|
||||||
|
// Update the recommended radius based on type
|
||||||
|
switch (type) {
|
||||||
|
case PatrolUnitTypeExtended.car:
|
||||||
|
patrolRadiusController.text = '6500';
|
||||||
|
break;
|
||||||
|
case PatrolUnitTypeExtended.motorcycle:
|
||||||
|
patrolRadiusController.text = '4000';
|
||||||
|
break;
|
||||||
|
case PatrolUnitTypeExtended.foot:
|
||||||
|
patrolRadiusController.text = '1000';
|
||||||
|
break;
|
||||||
|
case PatrolUnitTypeExtended.drone:
|
||||||
|
patrolRadiusController.text = '3000';
|
||||||
|
break;
|
||||||
|
case PatrolUnitTypeExtended.mixed:
|
||||||
|
patrolRadiusController.text = '4000';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(['patrol_type_selector', 'patrol_radius_field', 'patrol_summary']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join an existing patrol unit
|
||||||
|
void joinPatrolUnit(PatrolUnitModel patrolUnit) {
|
||||||
|
patrolUnitIdController.text = patrolUnit.id;
|
||||||
|
selectedPatrolUnitName.value = patrolUnit.name;
|
||||||
|
|
||||||
|
// Determine patrol type from the unit type
|
||||||
|
if (patrolUnit.type.toLowerCase() == 'car') {
|
||||||
|
selectedPatrolType.value = PatrolUnitTypeExtended.car;
|
||||||
|
} else if (patrolUnit.type.toLowerCase() == 'motorcycle') {
|
||||||
|
selectedPatrolType.value = PatrolUnitTypeExtended.motorcycle;
|
||||||
|
} else if (patrolUnit.type.toLowerCase() == 'foot') {
|
||||||
|
selectedPatrolType.value = PatrolUnitTypeExtended.foot;
|
||||||
|
} else if (patrolUnit.type.toLowerCase() == 'drone') {
|
||||||
|
selectedPatrolType.value = PatrolUnitTypeExtended.drone;
|
||||||
|
} else {
|
||||||
|
selectedPatrolType.value = PatrolUnitTypeExtended.mixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(['patrol_selection']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get available patrol units filtered by type AND category
|
||||||
|
List<PatrolUnitModel> getFilteredPatrolUnits() {
|
||||||
|
// Filter by vehicle type first
|
||||||
|
var filteredByType =
|
||||||
|
availablePatrolUnits
|
||||||
|
.where(
|
||||||
|
(unit) =>
|
||||||
|
unit.type.toLowerCase() ==
|
||||||
|
selectedPatrolType.value.name.toLowerCase(),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Then filter by category based on selection mode
|
||||||
|
if (patrolSelectionMode.value == PatrolSelectionMode.individual) {
|
||||||
|
return filteredByType
|
||||||
|
.where((unit) => unit.category?.toLowerCase() == 'individual')
|
||||||
|
.toList();
|
||||||
|
} else if (patrolSelectionMode.value == PatrolSelectionMode.group) {
|
||||||
|
return filteredByType
|
||||||
|
.where((unit) => unit.category?.toLowerCase() == 'group')
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to returning all units filtered by type if in create mode
|
||||||
|
return filteredByType;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate new patrol unit data with enhanced validation
|
||||||
|
bool validateNewPatrolUnit() {
|
||||||
|
bool isValid = true;
|
||||||
|
|
||||||
|
// Clear previous errors
|
||||||
|
clearErrors();
|
||||||
|
|
||||||
|
// 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 status
|
||||||
|
if (patrolStatusController.text.isEmpty) {
|
||||||
|
patrolStatusError.value = 'Status is required';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate member count
|
||||||
|
if (memberCountController.text.isEmpty) {
|
||||||
|
memberCountError.value = 'Member count is required';
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
final count = int.parse(memberCountController.text);
|
||||||
|
if (count <= 0) {
|
||||||
|
memberCountError.value = 'Must have at least 1 member';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
memberCountError.value = 'Invalid member count';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate category
|
||||||
|
if (categoryController.text.isEmpty) {
|
||||||
|
categoryError.value = 'Category is required';
|
||||||
|
isValid = false;
|
||||||
|
} else if (categoryController.text != 'individual' &&
|
||||||
|
categoryController.text != 'group') {
|
||||||
|
categoryError.value = 'Category must be either individual or group';
|
||||||
|
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;
|
||||||
|
} else {
|
||||||
|
// Validate against recommended ranges for each type
|
||||||
|
final String? rangeError = validateRadiusRange(
|
||||||
|
patrolTypeController.text.toLowerCase(),
|
||||||
|
radius,
|
||||||
|
);
|
||||||
|
if (rangeError != null) {
|
||||||
|
patrolRadiusError.value = rangeError;
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
patrolRadiusError.value = 'Invalid radius value';
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate radius against recommended range
|
||||||
|
String? validateRadiusRange(String patrolType, double radius) {
|
||||||
|
switch (patrolType) {
|
||||||
|
case 'car':
|
||||||
|
if (radius < 5000)
|
||||||
|
return 'Car patrols should have at least 5000m radius';
|
||||||
|
if (radius > 8000) return 'Car patrol radius should not exceed 8000m';
|
||||||
|
break;
|
||||||
|
case 'motorcycle':
|
||||||
|
if (radius < 3000)
|
||||||
|
return 'Motorcycle patrols should have at least 3000m radius';
|
||||||
|
if (radius > 5000)
|
||||||
|
return 'Motorcycle patrol radius should not exceed 5000m';
|
||||||
|
break;
|
||||||
|
case 'foot':
|
||||||
|
if (radius < 500)
|
||||||
|
return 'Foot patrols should have at least 500m radius';
|
||||||
|
if (radius > 1500) return 'Foot patrol radius should not exceed 1500m';
|
||||||
|
break;
|
||||||
|
case 'drone':
|
||||||
|
if (radius < 2000)
|
||||||
|
return 'Drone patrols should have at least 2000m radius';
|
||||||
|
if (radius > 4000) return 'Drone patrol radius should not exceed 4000m';
|
||||||
|
break;
|
||||||
|
case 'mixed':
|
||||||
|
if (radius < 2000)
|
||||||
|
return 'Mixed patrols should have at least 2000m radius';
|
||||||
|
if (radius > 6000) return 'Mixed patrol radius should not exceed 6000m';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recommended range text based on patrol type
|
||||||
|
String getRecommendedRangeText(String patrolType) {
|
||||||
|
switch (patrolType) {
|
||||||
|
case 'car':
|
||||||
|
return '5000-8000m';
|
||||||
|
case 'motorcycle':
|
||||||
|
return '3000-5000m';
|
||||||
|
case 'foot':
|
||||||
|
return '500-1500m';
|
||||||
|
case 'drone':
|
||||||
|
return '2000-4000m';
|
||||||
|
case 'mixed':
|
||||||
|
return '2000-6000m';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new patrol unit with enhanced fields
|
||||||
|
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 {
|
||||||
|
// Generate a proper ID for the new patrol unit
|
||||||
|
final String typeCode =
|
||||||
|
patrolTypeController.text.isNotEmpty
|
||||||
|
? patrolTypeController.text[0].toUpperCase()
|
||||||
|
: 'X';
|
||||||
|
|
||||||
|
final String unitCode =
|
||||||
|
unitIdController.text.length >= 2
|
||||||
|
? unitIdController.text.substring(
|
||||||
|
unitIdController.text.length - 2,
|
||||||
|
)
|
||||||
|
: 'XX';
|
||||||
|
|
||||||
|
// Generate a sequence number based on current time
|
||||||
|
final int rawSequence = DateTime.now().millisecondsSinceEpoch % 100000;
|
||||||
|
final String sequence = rawSequence
|
||||||
|
.toString()
|
||||||
|
.padLeft(2, '0')
|
||||||
|
.substring(0, rawSequence.toString().length.clamp(2, 5));
|
||||||
|
|
||||||
|
final String patrolUnitId = 'PU-$typeCode$unitCode$sequence';
|
||||||
|
|
||||||
|
// In a real scenario, we would create the patrol unit in the database
|
||||||
|
// Here we're simulating a successful creation
|
||||||
|
|
||||||
|
// Create patrol unit model
|
||||||
|
final newPatrolUnit = PatrolUnitModel(
|
||||||
|
id: patrolUnitId,
|
||||||
|
unitId: unitIdController.text,
|
||||||
|
locationId:
|
||||||
|
'', // This would come from the location selection or current location
|
||||||
|
name: patrolNameController.text,
|
||||||
|
type: patrolTypeController.text,
|
||||||
|
status: patrolStatusController.text,
|
||||||
|
radius: double.parse(patrolRadiusController.text),
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
memberCount: int.tryParse(memberCountController.text),
|
||||||
|
category: categoryController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
// In a real app, we would save this to the database
|
||||||
|
// final result = await patrolUnitRepository.createPatrolUnit(newPatrolUnit);
|
||||||
|
|
||||||
|
// Update the controller state
|
||||||
|
patrolUnitIdController.text = patrolUnitId;
|
||||||
|
selectedPatrolUnitName.value = patrolNameController.text;
|
||||||
|
|
||||||
|
// Simulate success
|
||||||
|
TLoaders.successSnackBar(
|
||||||
|
title: 'Success',
|
||||||
|
message: 'Patrol unit created successfully',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add to available patrol units for immediate display
|
||||||
|
availablePatrolUnits.add(newPatrolUnit);
|
||||||
|
|
||||||
|
update(['patrol_summary', 'patrol_selection']);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Error',
|
||||||
|
message: 'Failed to create patrol unit: $error',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all error messages
|
||||||
|
void clearErrors() {
|
||||||
|
patrolNameError.value = '';
|
||||||
|
patrolTypeError.value = '';
|
||||||
|
patrolRadiusError.value = '';
|
||||||
|
patrolStatusError.value = '';
|
||||||
|
memberCountError.value = '';
|
||||||
|
categoryError.value = '';
|
||||||
|
patrolUnitIdError.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the unit info (called from the parent controller)
|
||||||
|
void setUnitInfo(String unitId, String unitName) {
|
||||||
|
unitIdController.text = unitId;
|
||||||
|
selectedUnitName.value = unitName;
|
||||||
|
|
||||||
|
// Fetch patrol units for this unit
|
||||||
|
getPatrolUnitsByUnitId(unitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset all form fields
|
||||||
|
void resetForm() {
|
||||||
|
patrolNameController.text = '';
|
||||||
|
patrolTypeController.text = 'car';
|
||||||
|
patrolRadiusController.text = '6500';
|
||||||
|
patrolStatusController.text = 'active';
|
||||||
|
memberCountController.text = '1';
|
||||||
|
categoryController.text = 'individual';
|
||||||
|
patrolUnitIdController.text = '';
|
||||||
|
selectedPatrolUnitName.value = '';
|
||||||
|
selectedPatrolType.value = PatrolUnitTypeExtended.car;
|
||||||
|
patrolSelectionMode.value = PatrolSelectionMode.individual;
|
||||||
|
clearErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
// Dispose all controllers
|
||||||
|
patrolUnitIdController.dispose();
|
||||||
|
patrolNameController.dispose();
|
||||||
|
patrolTypeController.dispose();
|
||||||
|
patrolRadiusController.dispose();
|
||||||
|
patrolStatusController.dispose();
|
||||||
|
memberCountController.dispose();
|
||||||
|
categoryController.dispose();
|
||||||
|
unitIdController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
|
}
|
|
@ -178,6 +178,8 @@ class OfficerModel {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'OfficerModel(id: $id, name: $name, nrp: $nrp)';
|
return 'OfficerModel(id: $id, name: $name, nrp: $nrp)';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.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/features/map/data/models/models/locations_model.dart';
|
import 'package:sigap/src/features/map/data/models/models/locations_model.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart';
|
|
||||||
|
|
||||||
class PatrolUnitModel {
|
class PatrolUnitModel {
|
||||||
final String id;
|
final String id;
|
||||||
|
@ -14,6 +14,8 @@ class PatrolUnitModel {
|
||||||
final List<OfficerModel>? members;
|
final List<OfficerModel>? members;
|
||||||
final LocationModel? location;
|
final LocationModel? location;
|
||||||
final UnitModel? unit;
|
final UnitModel? unit;
|
||||||
|
final String? category; // Added category field (individual/group)
|
||||||
|
final int? memberCount; // Added member count field
|
||||||
|
|
||||||
PatrolUnitModel({
|
PatrolUnitModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
@ -27,6 +29,8 @@ class PatrolUnitModel {
|
||||||
this.members,
|
this.members,
|
||||||
this.location,
|
this.location,
|
||||||
this.unit,
|
this.unit,
|
||||||
|
this.category,
|
||||||
|
this.memberCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create a PatrolUnitModel instance from a JSON object
|
// Create a PatrolUnitModel instance from a JSON object
|
||||||
|
@ -54,6 +58,11 @@ class PatrolUnitModel {
|
||||||
json['unit'] != null
|
json['unit'] != null
|
||||||
? UnitModel.fromJson(json['unit'] as Map<String, dynamic>)
|
? UnitModel.fromJson(json['unit'] as Map<String, dynamic>)
|
||||||
: null,
|
: null,
|
||||||
|
category: json['category'] as String?, // Parse category from JSON
|
||||||
|
memberCount:
|
||||||
|
json['member_count'] != null
|
||||||
|
? int.parse(json['member_count'].toString())
|
||||||
|
: null, // Parse member_count from JSON
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,6 +78,8 @@ class PatrolUnitModel {
|
||||||
'radius': radius,
|
'radius': radius,
|
||||||
'created_at': createdAt.toIso8601String(),
|
'created_at': createdAt.toIso8601String(),
|
||||||
if (members != null) 'members': members!.map((e) => e.toJson()).toList(),
|
if (members != null) 'members': members!.map((e) => e.toJson()).toList(),
|
||||||
|
if (category != null) 'category': category,
|
||||||
|
if (memberCount != null) 'member_count': memberCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +96,8 @@ class PatrolUnitModel {
|
||||||
List<OfficerModel>? members,
|
List<OfficerModel>? members,
|
||||||
LocationModel? location,
|
LocationModel? location,
|
||||||
UnitModel? unit,
|
UnitModel? unit,
|
||||||
|
String? category,
|
||||||
|
int? memberCount,
|
||||||
}) {
|
}) {
|
||||||
return PatrolUnitModel(
|
return PatrolUnitModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
@ -98,6 +111,8 @@ class PatrolUnitModel {
|
||||||
members: members ?? this.members,
|
members: members ?? this.members,
|
||||||
location: location ?? this.location,
|
location: location ?? this.location,
|
||||||
unit: unit ?? this.unit,
|
unit: unit ?? this.unit,
|
||||||
|
category: category ?? this.category,
|
||||||
|
memberCount: memberCount ?? this.memberCount,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,8 +120,14 @@ class PatrolUnitModel {
|
||||||
return {'latitude': location?.latitude, 'longitude': location?.longitude};
|
return {'latitude': location?.latitude, 'longitude': location?.longitude};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate member count if it's not provided but members list exists
|
||||||
|
int get effectiveMemberCount {
|
||||||
|
if (memberCount != null) return memberCount!;
|
||||||
|
return members?.length ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'PatrolUnitModel(id: $id, name: $name, status: $status)';
|
return 'PatrolUnitModel(id: $id, name: $name, status: $status, category: $category, memberCount: $memberCount)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,48 +44,28 @@ class OfficerRepository extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update officer profile
|
// Update officer profile
|
||||||
Future<OfficerModel> updateOfficerProfile(OfficerModel officer) async {
|
Future<OfficerModel?> updateOfficer(OfficerModel officer) async {
|
||||||
try {
|
try {
|
||||||
if (currentUserId == null) {
|
if (currentUserId == null) {
|
||||||
throw 'User not authenticated';
|
throw 'User not authenticated';
|
||||||
}
|
}
|
||||||
|
|
||||||
final updatedOfficerData = {
|
final data = officer.toJson();
|
||||||
'name': officer.name,
|
|
||||||
'nrp': officer.nrp,
|
|
||||||
'rank': officer.rank,
|
|
||||||
'position': officer.position,
|
|
||||||
'phone': officer.phone,
|
|
||||||
'email': officer.email,
|
|
||||||
'avatar': officer.avatar,
|
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await _supabase
|
final updatedOfficer =
|
||||||
|
await _supabase
|
||||||
.from('officers')
|
.from('officers')
|
||||||
.update(updatedOfficerData)
|
.update(data)
|
||||||
.eq('id', currentUserId!);
|
.eq('id', currentUserId!)
|
||||||
|
.select()
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
// Also update the user's metadata to keep it in sync
|
if (updatedOfficer == null) {
|
||||||
final currentMetadata = _supabase.auth.currentUser?.userMetadata ?? {};
|
return null;
|
||||||
final officerDataInMetadata = currentMetadata['officer_data'] ?? {};
|
}
|
||||||
|
|
||||||
final updatedOfficerMetadata = {
|
// updatedOfficer is a List, so we take the first item and convert it
|
||||||
...officerDataInMetadata,
|
return OfficerModel.fromJson(updatedOfficer);
|
||||||
'name': officer.name,
|
|
||||||
'nrp': officer.nrp,
|
|
||||||
'rank': officer.rank,
|
|
||||||
'position': officer.position,
|
|
||||||
'phone': officer.phone,
|
|
||||||
};
|
|
||||||
|
|
||||||
await _supabase.auth.updateUser(
|
|
||||||
UserAttributes(
|
|
||||||
data: {...currentMetadata, 'officer_data': updatedOfficerMetadata},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return await getOfficerData();
|
|
||||||
} on PostgrestException catch (error) {
|
} on PostgrestException catch (error) {
|
||||||
throw TExceptions.fromCode(error.code!);
|
throw TExceptions.fromCode(error.code!);
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||||
import 'package:logger/Logger.dart';
|
import 'package:logger/Logger.dart';
|
||||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/roles_model.dart';
|
||||||
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/users_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/users_model.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
||||||
|
@ -214,11 +215,12 @@ class UserRepository extends GetxController {
|
||||||
|
|
||||||
final role = RoleModel.fromJson(roleData);
|
final role = RoleModel.fromJson(roleData);
|
||||||
|
|
||||||
// Update metadata with role information
|
final metadata =
|
||||||
await updateUserMetadata({
|
UserMetadataModel(
|
||||||
'is_officer': role.isOfficer,
|
isOfficer: role.isOfficer,
|
||||||
'role_name': role.name,
|
roleId: role.id,
|
||||||
});
|
).toAuthMetadataJson();
|
||||||
|
await updateUserMetadata(metadata);
|
||||||
} on PostgrestException catch (error) {
|
} on PostgrestException catch (error) {
|
||||||
_logger.e('PostgrestException in updateUserRole: ${error.message}');
|
_logger.e('PostgrestException in updateUserRole: ${error.message}');
|
||||||
throw TExceptions.fromCode(error.code ?? 'unknown-error');
|
throw TExceptions.fromCode(error.code ?? 'unknown-error');
|
||||||
|
|
|
@ -55,7 +55,9 @@ class CustomTextField extends StatelessWidget {
|
||||||
|
|
||||||
// Determine the effective fill color
|
// Determine the effective fill color
|
||||||
final Color effectiveFillColor =
|
final Color effectiveFillColor =
|
||||||
fillColor ?? (isDark ? TColors.dark : TColors.lightContainer);
|
enabled == false
|
||||||
|
? (isDark ? Colors.grey[800]! : Colors.grey[200]!)
|
||||||
|
: fillColor ?? (isDark ? TColors.dark : TColors.lightContainer);
|
||||||
|
|
||||||
// Get the common input decoration for both cases
|
// Get the common input decoration for both cases
|
||||||
final inputDecoration = _getInputDecoration(
|
final inputDecoration = _getInputDecoration(
|
||||||
|
@ -65,16 +67,26 @@ class CustomTextField extends StatelessWidget {
|
||||||
effectiveFillColor,
|
effectiveFillColor,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Style for disabled text
|
||||||
|
final TextStyle? textStyle =
|
||||||
|
enabled == false
|
||||||
|
? Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: isDark ? Colors.grey[500] : Colors.grey[600],
|
||||||
|
)
|
||||||
|
: Theme.of(context).textTheme.bodyMedium;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Label text using theme typography
|
// Label text with dimmed color if disabled
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: Theme.of(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
context,
|
|
||||||
).textTheme.bodyMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
|
color:
|
||||||
|
enabled == false
|
||||||
|
? (isDark ? Colors.grey[500] : Colors.grey[600])
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.sm),
|
const SizedBox(height: TSizes.sm),
|
||||||
|
@ -83,7 +95,7 @@ class CustomTextField extends StatelessWidget {
|
||||||
if (controller != null)
|
if (controller != null)
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
validator: validator,
|
validator: enabled == false ? null : validator,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
|
@ -91,13 +103,13 @@ class CustomTextField extends StatelessWidget {
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: textStyle,
|
||||||
decoration: inputDecoration,
|
decoration: inputDecoration,
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
TextFormField(
|
TextFormField(
|
||||||
initialValue: initialValue,
|
initialValue: initialValue,
|
||||||
validator: validator,
|
validator: enabled == false ? null : validator,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
textInputAction: textInputAction,
|
textInputAction: textInputAction,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
|
@ -105,7 +117,7 @@ class CustomTextField extends StatelessWidget {
|
||||||
readOnly: readOnly,
|
readOnly: readOnly,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: textStyle,
|
||||||
decoration: inputDecoration,
|
decoration: inputDecoration,
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.spaceBtwInputFields),
|
const SizedBox(height: TSizes.spaceBtwInputFields),
|
||||||
|
@ -119,6 +131,11 @@ class CustomTextField extends StatelessWidget {
|
||||||
bool isDark,
|
bool isDark,
|
||||||
Color effectiveFillColor,
|
Color effectiveFillColor,
|
||||||
) {
|
) {
|
||||||
|
final borderColor =
|
||||||
|
enabled == false
|
||||||
|
? (isDark ? Colors.grey[700] : Colors.grey[300])
|
||||||
|
: Theme.of(context).dividerColor;
|
||||||
|
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
@ -130,22 +147,49 @@ class CustomTextField extends StatelessWidget {
|
||||||
horizontal: TSizes.md,
|
horizontal: TSizes.md,
|
||||||
vertical: TSizes.md,
|
vertical: TSizes.md,
|
||||||
),
|
),
|
||||||
prefixIcon: prefixIcon,
|
prefixIcon:
|
||||||
suffixIcon: suffixIcon,
|
enabled == false && prefixIcon != null
|
||||||
|
? IconTheme(
|
||||||
|
data: IconThemeData(
|
||||||
|
color: isDark ? Colors.grey[600] : Colors.grey[400],
|
||||||
|
),
|
||||||
|
child: prefixIcon!,
|
||||||
|
)
|
||||||
|
: prefixIcon,
|
||||||
|
suffixIcon:
|
||||||
|
enabled == false && suffixIcon != null
|
||||||
|
? IconTheme(
|
||||||
|
data: IconThemeData(
|
||||||
|
color: isDark ? Colors.grey[600] : Colors.grey[400],
|
||||||
|
),
|
||||||
|
child: suffixIcon!,
|
||||||
|
)
|
||||||
|
: suffixIcon,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: effectiveFillColor,
|
fillColor: effectiveFillColor,
|
||||||
// Use theme-aware border styling
|
// Use theme-aware border styling with different styling for disabled state
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: Theme.of(context).dividerColor, width: 1),
|
borderSide: BorderSide(color: borderColor!, width: 1),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: Theme.of(context).dividerColor, width: 1),
|
borderSide: BorderSide(color: borderColor, width: 1),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
borderSide: BorderSide(color: effectiveAccentColor, width: 1.5),
|
borderSide: BorderSide(
|
||||||
|
color: enabled == false ? borderColor : effectiveAccentColor,
|
||||||
|
width: enabled == false ? 1 : 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
|
borderSide: BorderSide(
|
||||||
|
color: borderColor,
|
||||||
|
width: 1,
|
||||||
|
style: BorderStyle.solid,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
errorBorder: OutlineInputBorder(
|
errorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
|
|
|
@ -33,6 +33,10 @@ class TColors {
|
||||||
static const Color error = Color(0xFFEF4444);
|
static const Color error = Color(0xFFEF4444);
|
||||||
static const Color success = Color(0xFF38B2AC);
|
static const Color success = Color(0xFF38B2AC);
|
||||||
static const Color warning = Color(0xFFF59E0B);
|
static const Color warning = Color(0xFFF59E0B);
|
||||||
|
static const Color info = Color(0xFF2563EB); // Information state color
|
||||||
|
static const Color disabled = Color(0xFFB0B0B0); // Disabled state color
|
||||||
|
static const Color active = Color(0xFF2F2F2F); // Active state color
|
||||||
|
static const Color inactive = Color(0xFF6B6B6B); // Inactive state color
|
||||||
|
|
||||||
// Neutral Shades
|
// Neutral Shades
|
||||||
static const Color black = Color(0xFF232323);
|
static const Color black = Color(0xFF232323);
|
||||||
|
@ -77,6 +81,9 @@ class TColors {
|
||||||
0xFFFFFFFF,
|
0xFFFFFFFF,
|
||||||
); // Dark mode card text
|
); // Dark mode card text
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Additional colors
|
// Additional colors
|
||||||
static const Color transparent = Colors.transparent;
|
static const Color transparent = Colors.transparent;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue