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 =
|
||||
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
|
||||
if (await _locationService.isLocationValidForFeature() == false) {
|
||||
_navigateToRoute(AppRoutes.locationWarning);
|
||||
|
@ -674,7 +669,7 @@ class AuthenticationRepository extends GetxController {
|
|||
// Don't attempt profile completion while already redirecting
|
||||
throw 'Cannot complete profile during redirection. Please try again.';
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Convert to UserModel
|
||||
final userMetadataModel = UserMetadataModel.fromInitUserMetadata(
|
||||
|
@ -697,7 +692,7 @@ class AuthenticationRepository extends GetxController {
|
|||
.from('profiles')
|
||||
.insert(completeData.viewerData!.toJson());
|
||||
}
|
||||
|
||||
|
||||
// Set redirection flag to ensure we don't navigate before setup is complete
|
||||
_isRedirecting = true;
|
||||
} catch (e) {
|
||||
|
|
|
@ -1,94 +1,67 @@
|
|||
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/models/models/units_model.dart';
|
||||
import 'package:sigap/src/features/daily-ops/data/repositories/patrol_units_repository.dart';
|
||||
import 'package:sigap/src/features/auth/data/models/kta_model.dart';
|
||||
import 'package:sigap/src/features/auth/data/repositories/authentication_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/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/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 {
|
||||
// Singleton instance
|
||||
static OfficerInfoController get instance => Get.find();
|
||||
|
||||
// Static form key
|
||||
// final GlobalKey<FormState> formKey = TGlobalFormKey.officerInfo();
|
||||
|
||||
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 for officer info
|
||||
final nrpController = TextEditingController();
|
||||
final rankController = TextEditingController();
|
||||
final unitIdController = TextEditingController();
|
||||
final patrolUnitIdController = TextEditingController();
|
||||
final nameController = TextEditingController();
|
||||
final rankController = TextEditingController();
|
||||
final positionController = TextEditingController();
|
||||
final phoneController = TextEditingController();
|
||||
final emailController = TextEditingController();
|
||||
final unitIdController = TextEditingController();
|
||||
final validUntilController = TextEditingController();
|
||||
final avatarController = TextEditingController();
|
||||
final qrCodeController = TextEditingController();
|
||||
final bannedReasonController = TextEditingController();
|
||||
final bannedUntilController = TextEditingController();
|
||||
|
||||
// Controllers for new patrol unit
|
||||
final patrolNameController = TextEditingController();
|
||||
final patrolTypeController = TextEditingController();
|
||||
final patrolRadiusController = TextEditingController(
|
||||
text: '500',
|
||||
); // Default radius in meters
|
||||
// New fields based on the model
|
||||
final placeOfBirthController = TextEditingController();
|
||||
final dateOfBirthController = TextEditingController();
|
||||
|
||||
// Error states
|
||||
final RxString nrpError = ''.obs;
|
||||
final RxString nameError = ''.obs;
|
||||
final RxString rankError = ''.obs;
|
||||
final RxString unitIdError = ''.obs;
|
||||
final RxString patrolUnitIdError = ''.obs;
|
||||
final RxString nameError = ''.obs;
|
||||
final RxString positionError = ''.obs;
|
||||
final RxString phoneError = ''.obs;
|
||||
final RxString emailError = ''.obs;
|
||||
final RxString validUntilError = ''.obs;
|
||||
final RxString avatarError = ''.obs;
|
||||
final RxString qrCodeError = ''.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;
|
||||
final RxString placeOfBirthError = ''.obs;
|
||||
final RxString dateOfBirthError = ''.obs;
|
||||
|
||||
// Logger instance
|
||||
final Logger logger = Logger();
|
||||
|
||||
// Make sure repositories are properly initialized
|
||||
// Unit repository
|
||||
late final UnitRepository unitRepository;
|
||||
late final PatrolUnitRepository patrolUnitRepository;
|
||||
|
||||
// Dropdown open state
|
||||
RxBool isUnitDropdownOpen = false.obs;
|
||||
|
||||
// Date selection related
|
||||
Rx<DateTime?> selectedValidUntil = Rx<DateTime?>(null);
|
||||
Rx<DateTime?> selectedDateOfBirth = Rx<DateTime?>(null);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
@ -102,9 +75,7 @@ class OfficerInfoController extends GetxController {
|
|||
void initRepositories() {
|
||||
// Check if repositories are already registered with GetX
|
||||
unitRepository = Get.find<UnitRepository>();
|
||||
patrolUnitRepository = Get.find<PatrolUnitRepository>();
|
||||
|
||||
Logger().i('UnitRepository and PatrolUnitRepository initialized');
|
||||
logger.i('UnitRepository initialized');
|
||||
}
|
||||
|
||||
// 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
|
||||
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 valid until date
|
||||
void setValidUntilDate(DateTime date) {
|
||||
selectedValidUntil.value = date;
|
||||
validUntilController.text = formatDate(date);
|
||||
validUntilError.value = '';
|
||||
}
|
||||
|
||||
// Set patrol unit type (car or motorcycle)
|
||||
void setPatrolUnitType(PatrolUnitType type) {
|
||||
selectedPatrolType.value = type;
|
||||
patrolTypeController.text = type.name;
|
||||
// Set date of birth
|
||||
void setDateOfBirth(DateTime date) {
|
||||
selectedDateOfBirth.value = date;
|
||||
dateOfBirthController.text = formatDate(date);
|
||||
dateOfBirthError.value = '';
|
||||
}
|
||||
|
||||
// Set patrol selection mode (individual, group, or create new)
|
||||
void setPatrolSelectionMode(PatrolSelectionMode mode) {
|
||||
patrolSelectionMode.value = mode;
|
||||
// Format date as yyyy-MM-dd
|
||||
String formatDate(DateTime date) {
|
||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// Reset fields when changing modes
|
||||
if (mode == PatrolSelectionMode.createNew) {
|
||||
isCreatingNewPatrolUnit.value = true;
|
||||
patrolUnitIdController.clear();
|
||||
selectedPatrolUnitName.value = '';
|
||||
} else {
|
||||
isCreatingNewPatrolUnit.value = false;
|
||||
// Populate fields from KTA data
|
||||
void populateFromKta(KtaModel ktaData) {
|
||||
if (ktaData.nrp.isNotEmpty && nrpController.text.isEmpty) {
|
||||
nrpController.text = ktaData.nrp;
|
||||
}
|
||||
|
||||
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
|
||||
bool validateNewPatrolUnit() {
|
||||
bool isValid = true;
|
||||
// Validate the form
|
||||
bool validate(GlobalKey<FormState>? formKey) {
|
||||
clearErrors();
|
||||
|
||||
// Clear previous errors
|
||||
patrolNameError.value = '';
|
||||
patrolTypeError.value = '';
|
||||
patrolRadiusError.value = '';
|
||||
bool isValid = formKey?.currentState?.validate() ?? false;
|
||||
|
||||
// Validate patrol unit name
|
||||
final nameValidation = TValidators.validateUserInput(
|
||||
'Patrol Unit Name',
|
||||
patrolNameController.text,
|
||||
50,
|
||||
);
|
||||
if (nameValidation != null) {
|
||||
patrolNameError.value = nameValidation;
|
||||
// Additional validation for required fields
|
||||
if (nrpController.text.isEmpty) {
|
||||
nrpError.value = 'NRP is required';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate patrol unit type
|
||||
if (patrolTypeController.text.isEmpty) {
|
||||
patrolTypeError.value = 'Patrol type is required';
|
||||
if (nameController.text.isEmpty) {
|
||||
nameError.value = 'Name 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;
|
||||
unitIdError.value = 'Please select a unit';
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
isFormValid.value = isValid;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
void updateOfficer(OfficerModel officer, UserMetadataModel metadata) async {
|
||||
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
|
||||
TCircularFullScreenLoader.openLoadingDialog();
|
||||
|
||||
// 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(),
|
||||
),
|
||||
final isConnected = await NetworkManager.instance.isConnected();
|
||||
if (!isConnected) {
|
||||
TLoaders.errorSnackBar(
|
||||
title: 'No Internet Connection',
|
||||
message: 'Please check your internet connection and try again.',
|
||||
);
|
||||
TCircularFullScreenLoader.stopLoading();
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate the form before proceeding
|
||||
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
|
||||
TLoaders.successSnackBar(
|
||||
title: 'Success',
|
||||
message: 'Patrol unit created successfully',
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
// final updatedOfficer = await OfficerRepository.instance.updateOfficer(
|
||||
// data,
|
||||
// );
|
||||
|
||||
// if (updatedOfficer == null) {
|
||||
// TLoaders.errorSnackBar(
|
||||
// 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(
|
||||
title: 'Error',
|
||||
message: 'Failed to create patrol unit: $error',
|
||||
title: 'Update Failed',
|
||||
message: 'An error occurred while updating officer information.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Join an existing patrol unit
|
||||
void joinPatrolUnit(PatrolUnitModel patrolUnit) {
|
||||
patrolUnitIdController.text = patrolUnit.id;
|
||||
selectedPatrolUnitName.value = patrolUnit.name;
|
||||
void resetForm() {
|
||||
nrpController.clear();
|
||||
nameController.clear();
|
||||
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();
|
||||
|
||||
if (formKey.currentState?.validate() ?? false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
selectedUnitName.value = '';
|
||||
selectedValidUntil.value = null;
|
||||
selectedDateOfBirth.value = null;
|
||||
}
|
||||
|
||||
// Clear all error messages
|
||||
void clearErrors() {
|
||||
nrpError.value = '';
|
||||
nameError.value = '';
|
||||
rankError.value = '';
|
||||
unitIdError.value = '';
|
||||
patrolUnitIdError.value = '';
|
||||
nameError.value = '';
|
||||
positionError.value = '';
|
||||
phoneError.value = '';
|
||||
emailError.value = '';
|
||||
validUntilError.value = '';
|
||||
avatarError.value = '';
|
||||
qrCodeError.value = '';
|
||||
bannedReasonError.value = '';
|
||||
bannedUntilError.value = '';
|
||||
|
||||
// Clear errors for new patrol unit fields
|
||||
patrolNameError.value = '';
|
||||
patrolTypeError.value = '';
|
||||
patrolRadiusError.value = '';
|
||||
placeOfBirthError.value = '';
|
||||
dateOfBirthError.value = '';
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
@override
|
||||
void onClose() {
|
||||
nrpController.dispose();
|
||||
rankController.dispose();
|
||||
unitIdController.dispose();
|
||||
patrolUnitIdController.dispose();
|
||||
nameController.dispose();
|
||||
rankController.dispose();
|
||||
positionController.dispose();
|
||||
phoneController.dispose();
|
||||
emailController.dispose();
|
||||
unitIdController.dispose();
|
||||
validUntilController.dispose();
|
||||
avatarController.dispose();
|
||||
qrCodeController.dispose();
|
||||
bannedReasonController.dispose();
|
||||
bannedUntilController.dispose();
|
||||
|
||||
// Dispose controllers for new patrol unit
|
||||
patrolNameController.dispose();
|
||||
patrolTypeController.dispose();
|
||||
patrolRadiusController.dispose();
|
||||
placeOfBirthController.dispose();
|
||||
dateOfBirthController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import 'package:flutter/material.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/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/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/text/custom_text_field.dart';
|
||||
import 'package:sigap/src/utils/constants/colors.dart';
|
||||
|
@ -19,6 +20,12 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
final mainController = Get.find<FormRegistrationController>();
|
||||
mainController.formKey = formKey;
|
||||
|
||||
// Check if KTA data exists and populate fields if available
|
||||
_populateFieldsFromKta(
|
||||
controller,
|
||||
mainController.idCardVerificationController,
|
||||
);
|
||||
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
|
@ -34,6 +41,7 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
// NRP field
|
||||
CustomTextField(
|
||||
label: 'NRP',
|
||||
enabled: controller.nrpController.text.isEmpty,
|
||||
controller: controller.nrpController,
|
||||
validator: TValidators.validateNRP,
|
||||
errorText: controller.nrpError.value,
|
||||
|
@ -45,24 +53,34 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
controller.nrpError.value = '';
|
||||
},
|
||||
),
|
||||
Obx(
|
||||
() =>
|
||||
controller.nrpError.value.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
controller.nrpError.value,
|
||||
style: TextStyle(color: Colors.red[700], fontSize: 12),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
_buildErrorText(controller.nrpError),
|
||||
|
||||
// Name field
|
||||
CustomTextField(
|
||||
label: 'Full Name',
|
||||
controller: controller.nameController,
|
||||
validator:
|
||||
(v) => TValidators.validateUserInput(
|
||||
'Name',
|
||||
v,
|
||||
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
|
||||
CustomTextField(
|
||||
label: 'Rank',
|
||||
controller: controller.rankController,
|
||||
validator: TValidators.validateRank,
|
||||
validator: (v) => TValidators.validateUserInput('Rank', v, 100),
|
||||
errorText: controller.rankError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., Captain',
|
||||
|
@ -71,30 +89,13 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
controller.rankError.value = '';
|
||||
},
|
||||
),
|
||||
Obx(
|
||||
() =>
|
||||
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(),
|
||||
),
|
||||
_buildErrorText(controller.rankError),
|
||||
|
||||
// Position field
|
||||
CustomTextField(
|
||||
label: 'Position',
|
||||
controller: controller.positionController,
|
||||
validator:
|
||||
(v) => TValidators.validateUserInput(
|
||||
'Position',
|
||||
v,
|
||||
100,
|
||||
required: true,
|
||||
),
|
||||
validator: (v) => TValidators.validateUserInput('Position', v, 100),
|
||||
errorText: controller.positionError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
hintText: 'e.g., Head of Unit',
|
||||
|
@ -103,18 +104,73 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
controller.positionError.value = '';
|
||||
},
|
||||
),
|
||||
Obx(
|
||||
() =>
|
||||
controller.positionError.value.isNotEmpty
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
controller.positionError.value,
|
||||
style: TextStyle(color: Colors.red[700], fontSize: 12),
|
||||
),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
_buildErrorText(controller.positionError),
|
||||
|
||||
// Phone field
|
||||
CustomTextField(
|
||||
label: 'Phone Number',
|
||||
controller: controller.phoneController,
|
||||
validator: TValidators.validatePhoneNumber,
|
||||
errorText: controller.phoneError.value,
|
||||
textInputAction: TextInputAction.next,
|
||||
keyboardType: TextInputType.phone,
|
||||
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),
|
||||
|
||||
|
@ -127,154 +183,142 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
const SizedBox(height: TSizes.spaceBtwItems),
|
||||
|
||||
// Unit dropdown
|
||||
_buildUnitDropdown(controller),
|
||||
_buildUnitDropdown(
|
||||
controller,
|
||||
mainController.idCardVerificationController,
|
||||
),
|
||||
|
||||
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',
|
||||
),
|
||||
),
|
||||
// Note about patrol unit assignment
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: TColors.info.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: TColors.info),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: TColors.info),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Patrol Unit Assignment',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: TColors.info,
|
||||
),
|
||||
|
||||
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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'After registration is complete, you will be able to join or create a patrol unit from the app.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
/// Populates form fields from KTA data if available
|
||||
void _populateFieldsFromKta(
|
||||
OfficerInfoController controller,
|
||||
IdCardVerificationController mainController,
|
||||
) {
|
||||
// Check if KTA data exists in the main controller
|
||||
final KtaModel? ktaData = mainController.ktaModel.value;
|
||||
|
||||
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
|
||||
Widget _buildUnitDropdown(OfficerInfoController controller) {
|
||||
Widget _buildUnitDropdown(
|
||||
OfficerInfoController controller,
|
||||
IdCardVerificationController idCardController,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
@ -351,6 +395,34 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
(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(
|
||||
children: [
|
||||
// Dropdown Selection Button
|
||||
|
@ -580,18 +652,7 @@ class OfficerInfoStep extends StatelessWidget {
|
|||
),
|
||||
|
||||
// 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),
|
||||
_buildErrorText(controller.unitIdError),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
|
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
|
||||
String toString() {
|
||||
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/map/data/models/models/locations_model.dart';
|
||||
import 'package:sigap/src/features/daily-ops/data/models/models/officers_model.dart';
|
||||
|
||||
class PatrolUnitModel {
|
||||
final String id;
|
||||
|
@ -14,6 +14,8 @@ class PatrolUnitModel {
|
|||
final List<OfficerModel>? members;
|
||||
final LocationModel? location;
|
||||
final UnitModel? unit;
|
||||
final String? category; // Added category field (individual/group)
|
||||
final int? memberCount; // Added member count field
|
||||
|
||||
PatrolUnitModel({
|
||||
required this.id,
|
||||
|
@ -27,6 +29,8 @@ class PatrolUnitModel {
|
|||
this.members,
|
||||
this.location,
|
||||
this.unit,
|
||||
this.category,
|
||||
this.memberCount,
|
||||
});
|
||||
|
||||
// Create a PatrolUnitModel instance from a JSON object
|
||||
|
@ -54,6 +58,11 @@ class PatrolUnitModel {
|
|||
json['unit'] != null
|
||||
? UnitModel.fromJson(json['unit'] as Map<String, dynamic>)
|
||||
: 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,
|
||||
'created_at': createdAt.toIso8601String(),
|
||||
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,
|
||||
LocationModel? location,
|
||||
UnitModel? unit,
|
||||
String? category,
|
||||
int? memberCount,
|
||||
}) {
|
||||
return PatrolUnitModel(
|
||||
id: id ?? this.id,
|
||||
|
@ -98,6 +111,8 @@ class PatrolUnitModel {
|
|||
members: members ?? this.members,
|
||||
location: location ?? this.location,
|
||||
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};
|
||||
}
|
||||
|
||||
// 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
|
||||
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
|
||||
Future<OfficerModel> updateOfficerProfile(OfficerModel officer) async {
|
||||
Future<OfficerModel?> updateOfficer(OfficerModel officer) async {
|
||||
try {
|
||||
if (currentUserId == null) {
|
||||
throw 'User not authenticated';
|
||||
}
|
||||
|
||||
final updatedOfficerData = {
|
||||
'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(),
|
||||
};
|
||||
final data = officer.toJson();
|
||||
|
||||
await _supabase
|
||||
final updatedOfficer =
|
||||
await _supabase
|
||||
.from('officers')
|
||||
.update(updatedOfficerData)
|
||||
.eq('id', currentUserId!);
|
||||
.update(data)
|
||||
.eq('id', currentUserId!)
|
||||
.select()
|
||||
.maybeSingle();
|
||||
|
||||
// Also update the user's metadata to keep it in sync
|
||||
final currentMetadata = _supabase.auth.currentUser?.userMetadata ?? {};
|
||||
final officerDataInMetadata = currentMetadata['officer_data'] ?? {};
|
||||
if (updatedOfficer == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final updatedOfficerMetadata = {
|
||||
...officerDataInMetadata,
|
||||
'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();
|
||||
// updatedOfficer is a List, so we take the first item and convert it
|
||||
return OfficerModel.fromJson(updatedOfficer);
|
||||
} on PostgrestException catch (error) {
|
||||
throw TExceptions.fromCode(error.code!);
|
||||
} on AuthException catch (e) {
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
|||
import 'package:logger/Logger.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/user_metadata_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/format_exceptions.dart';
|
||||
|
@ -214,11 +215,12 @@ class UserRepository extends GetxController {
|
|||
|
||||
final role = RoleModel.fromJson(roleData);
|
||||
|
||||
// Update metadata with role information
|
||||
await updateUserMetadata({
|
||||
'is_officer': role.isOfficer,
|
||||
'role_name': role.name,
|
||||
});
|
||||
final metadata =
|
||||
UserMetadataModel(
|
||||
isOfficer: role.isOfficer,
|
||||
roleId: role.id,
|
||||
).toAuthMetadataJson();
|
||||
await updateUserMetadata(metadata);
|
||||
} on PostgrestException catch (error) {
|
||||
_logger.e('PostgrestException in updateUserRole: ${error.message}');
|
||||
throw TExceptions.fromCode(error.code ?? 'unknown-error');
|
||||
|
|
|
@ -55,7 +55,9 @@ class CustomTextField extends StatelessWidget {
|
|||
|
||||
// Determine the effective fill color
|
||||
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
|
||||
final inputDecoration = _getInputDecoration(
|
||||
|
@ -65,16 +67,26 @@ class CustomTextField extends StatelessWidget {
|
|||
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(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Label text using theme typography
|
||||
// Label text with dimmed color if disabled
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color:
|
||||
enabled == false
|
||||
? (isDark ? Colors.grey[500] : Colors.grey[600])
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: TSizes.sm),
|
||||
|
@ -83,7 +95,7 @@ class CustomTextField extends StatelessWidget {
|
|||
if (controller != null)
|
||||
TextFormField(
|
||||
controller: controller,
|
||||
validator: validator,
|
||||
validator: enabled == false ? null : validator,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
maxLines: maxLines,
|
||||
|
@ -91,13 +103,13 @@ class CustomTextField extends StatelessWidget {
|
|||
readOnly: readOnly,
|
||||
obscureText: obscureText,
|
||||
onChanged: onChanged,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: textStyle,
|
||||
decoration: inputDecoration,
|
||||
)
|
||||
else
|
||||
TextFormField(
|
||||
initialValue: initialValue,
|
||||
validator: validator,
|
||||
validator: enabled == false ? null : validator,
|
||||
keyboardType: keyboardType,
|
||||
textInputAction: textInputAction,
|
||||
maxLines: maxLines,
|
||||
|
@ -105,7 +117,7 @@ class CustomTextField extends StatelessWidget {
|
|||
readOnly: readOnly,
|
||||
obscureText: obscureText,
|
||||
onChanged: onChanged,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style: textStyle,
|
||||
decoration: inputDecoration,
|
||||
),
|
||||
const SizedBox(height: TSizes.spaceBtwInputFields),
|
||||
|
@ -119,6 +131,11 @@ class CustomTextField extends StatelessWidget {
|
|||
bool isDark,
|
||||
Color effectiveFillColor,
|
||||
) {
|
||||
final borderColor =
|
||||
enabled == false
|
||||
? (isDark ? Colors.grey[700] : Colors.grey[300])
|
||||
: Theme.of(context).dividerColor;
|
||||
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
|
@ -130,22 +147,49 @@ class CustomTextField extends StatelessWidget {
|
|||
horizontal: TSizes.md,
|
||||
vertical: TSizes.md,
|
||||
),
|
||||
prefixIcon: prefixIcon,
|
||||
suffixIcon: suffixIcon,
|
||||
prefixIcon:
|
||||
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,
|
||||
fillColor: effectiveFillColor,
|
||||
// Use theme-aware border styling
|
||||
// Use theme-aware border styling with different styling for disabled state
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor, width: 1),
|
||||
borderSide: BorderSide(color: borderColor!, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
borderSide: BorderSide(color: Theme.of(context).dividerColor, width: 1),
|
||||
borderSide: BorderSide(color: borderColor, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
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(
|
||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||
|
|
|
@ -33,6 +33,10 @@ class TColors {
|
|||
static const Color error = Color(0xFFEF4444);
|
||||
static const Color success = Color(0xFF38B2AC);
|
||||
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
|
||||
static const Color black = Color(0xFF232323);
|
||||
|
@ -77,6 +81,9 @@ class TColors {
|
|||
0xFFFFFFFF,
|
||||
); // Dark mode card text
|
||||
|
||||
|
||||
|
||||
|
||||
// Additional colors
|
||||
static const Color transparent = Colors.transparent;
|
||||
|
||||
|
|
Loading…
Reference in New Issue