feat: Enhance officer information signup form with additional fields and validation
- Added new text controllers and error states for unit ID, patrol unit ID, name, position, phone, email, valid until, avatar, QR code, banned reason, and banned until. - Implemented validation logic for the new fields in the officer info controller. - Updated the signup form screen to include new input fields for unit ID and patrol unit ID. - Refactored the signup with role screen to improve layout and remove unnecessary components. - Introduced a new role signup page view to manage role selection and signup process. - Updated the welcome screen button text to reflect new functionality. - Adjusted the primary color in the dark theme for better visibility. - Enhanced the auth button height for improved usability.
This commit is contained in:
parent
6a4813c15e
commit
dd2bb57e42
|
@ -10,6 +10,7 @@ import 'package:sigap/src/features/auth/presentasion/pages/signup/step/selfie-ve
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/location-warning/location_warning_screen.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart';
|
||||||
|
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ class AppPages {
|
||||||
// Auth
|
// Auth
|
||||||
GetPage(
|
GetPage(
|
||||||
name: AppRoutes.roleSelection,
|
name: AppRoutes.roleSelection,
|
||||||
page: () => const RoleSelectionScreen(),
|
page: () => const RoleSignupPageView(),
|
||||||
),
|
),
|
||||||
|
|
||||||
GetPage(
|
GetPage(
|
||||||
|
|
|
@ -140,7 +140,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
if (isFirstTime) {
|
if (isFirstTime) {
|
||||||
_navigateToRoute(AppRoutes.onboarding);
|
_navigateToRoute(AppRoutes.onboarding);
|
||||||
} else {
|
} else {
|
||||||
_navigateToRoute(AppRoutes.onboarding);
|
_navigateToRoute(AppRoutes.signIn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:get/get.dart';
|
||||||
import 'package:get_storage/get_storage.dart';
|
import 'package:get_storage/get_storage.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
|
import 'package:sigap/src/features/onboarding/presentasion/controllers/role_selection_controller.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
import 'package:sigap/src/features/personalization/data/models/index.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/personalization/data/models/models/user_metadata_model.dart';
|
||||||
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
|
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
|
||||||
|
@ -53,12 +54,6 @@ class SignupWithRoleController extends GetxController {
|
||||||
// Check if Apple Sign In is available (only on iOS)
|
// Check if Apple Sign In is available (only on iOS)
|
||||||
final RxBool isAppleSignInAvailable = RxBool(Platform.isIOS);
|
final RxBool isAppleSignInAvailable = RxBool(Platform.isIOS);
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
loadRoles();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
emailController.dispose();
|
emailController.dispose();
|
||||||
|
@ -67,55 +62,6 @@ class SignupWithRoleController extends GetxController {
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load available roles
|
|
||||||
Future<void> loadRoles() async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final roles = await roleRepository.getAllRoles();
|
|
||||||
availableRoles.assignAll(roles);
|
|
||||||
|
|
||||||
// Pre-select the default roles based on roleType
|
|
||||||
_updateSelectedRoleBasedOnType();
|
|
||||||
} catch (e) {
|
|
||||||
Logger().e('Error loading roles: $e');
|
|
||||||
TLoaders.errorSnackBar(
|
|
||||||
title: 'Error',
|
|
||||||
message: 'Failed to load roles. Please try again.',
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set role type (viewer or officer)
|
|
||||||
void setRoleType(RoleType type) {
|
|
||||||
roleType.value = type;
|
|
||||||
_updateSelectedRoleBasedOnType();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update selected role based on roleType
|
|
||||||
void _updateSelectedRoleBasedOnType() {
|
|
||||||
if (availableRoles.isNotEmpty) {
|
|
||||||
if (roleType.value == RoleType.officer) {
|
|
||||||
// Find an officer role
|
|
||||||
final officerRole = availableRoles.firstWhere(
|
|
||||||
(role) => role.isOfficer,
|
|
||||||
orElse: () => availableRoles.first,
|
|
||||||
);
|
|
||||||
selectedRoleId.value = officerRole.id;
|
|
||||||
selectedRole.value = officerRole;
|
|
||||||
} else {
|
|
||||||
// Find a viewer role
|
|
||||||
final viewerRole = availableRoles.firstWhere(
|
|
||||||
(role) => !role.isOfficer,
|
|
||||||
orElse: () => availableRoles.first,
|
|
||||||
);
|
|
||||||
selectedRoleId.value = viewerRole.id;
|
|
||||||
selectedRole.value = viewerRole;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validators
|
// Validators
|
||||||
String? validateEmail(String? value) {
|
String? validateEmail(String? value) {
|
||||||
final error = TValidators.validateEmail(value);
|
final error = TValidators.validateEmail(value);
|
||||||
|
@ -168,7 +114,7 @@ class SignupWithRoleController extends GetxController {
|
||||||
|
|
||||||
// Sign up function
|
// Sign up function
|
||||||
/// Updated signup function with better error handling and argument passing
|
/// Updated signup function with better error handling and argument passing
|
||||||
void signUp(bool isOfficer) async {
|
void signUp() async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
Logger().i('SignUp process started');
|
Logger().i('SignUp process started');
|
||||||
|
@ -196,13 +142,16 @@ class SignupWithRoleController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have a role selected
|
// Get selected role info before authentication
|
||||||
if (selectedRoleId.value.isEmpty) {
|
|
||||||
_updateSelectedRoleBasedOnType();
|
final roleController = RoleSelectionController.instance;
|
||||||
}
|
final isOfficer = roleController.isOfficer.value;
|
||||||
|
final roleId = roleController.selectedRole.value!.id;
|
||||||
|
|
||||||
|
Logger().i('Is officer: $isOfficer');
|
||||||
|
|
||||||
// Validate role selection
|
// Validate role selection
|
||||||
if (selectedRoleId.value.isEmpty) {
|
if (roleId.isEmpty) {
|
||||||
TLoaders.errorSnackBar(
|
TLoaders.errorSnackBar(
|
||||||
title: 'Role Required',
|
title: 'Role Required',
|
||||||
message: 'Please select a role before continuing.',
|
message: 'Please select a role before continuing.',
|
||||||
|
@ -213,11 +162,13 @@ class SignupWithRoleController extends GetxController {
|
||||||
// Create comprehensive initial user metadata
|
// Create comprehensive initial user metadata
|
||||||
final initialMetadata = UserMetadataModel(
|
final initialMetadata = UserMetadataModel(
|
||||||
email: emailController.text.trim(),
|
email: emailController.text.trim(),
|
||||||
roleId: selectedRoleId.value,
|
roleId: roleId,
|
||||||
isOfficer: isOfficer,
|
isOfficer: isOfficer,
|
||||||
profileStatus: 'incomplete',
|
profileStatus: 'incomplete',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Logger().i('Initial metadata: ${initialMetadata.toJson()}');
|
||||||
|
|
||||||
// Create the account
|
// Create the account
|
||||||
final authResponse = await AuthenticationRepository.instance
|
final authResponse = await AuthenticationRepository.instance
|
||||||
.initialSignUp(
|
.initialSignUp(
|
||||||
|
@ -238,11 +189,6 @@ class SignupWithRoleController extends GetxController {
|
||||||
final user = authResponse.user!;
|
final user = authResponse.user!;
|
||||||
Logger().d('Account created successfully for user: ${user.id}');
|
Logger().d('Account created successfully for user: ${user.id}');
|
||||||
|
|
||||||
// Store temporary data for verification process
|
|
||||||
await _storeTemporaryData(authResponse, isOfficer);
|
|
||||||
|
|
||||||
// Navigate with arguments
|
|
||||||
Logger().i('Navigating to registration form');
|
|
||||||
AuthenticationRepository.instance.screenRedirect();
|
AuthenticationRepository.instance.screenRedirect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger().e('Error during signup: $e');
|
Logger().e('Error during signup: $e');
|
||||||
|
@ -264,24 +210,6 @@ class SignupWithRoleController extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Store temporary data for the verification process
|
|
||||||
Future<void> _storeTemporaryData(
|
|
||||||
AuthResponse authResponse,
|
|
||||||
bool isOfficer,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
await storage.write('CURRENT_USER_EMAIL', emailController.text.trim());
|
|
||||||
await storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken);
|
|
||||||
await storage.write('TEMP_USER_ID', authResponse.user?.id);
|
|
||||||
await storage.write('TEMP_ROLE_ID', selectedRoleId.value);
|
|
||||||
await storage.write('TEMP_IS_OFFICER', isOfficer);
|
|
||||||
|
|
||||||
Logger().d('Temporary data stored successfully');
|
|
||||||
} catch (e) {
|
|
||||||
Logger().e('Failed to store temporary data: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sign in with Google
|
// Sign in with Google
|
||||||
Future<void> signInWithGoogle() async {
|
Future<void> signInWithGoogle() async {
|
||||||
try {
|
try {
|
||||||
|
@ -297,14 +225,10 @@ class SignupWithRoleController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get selected role info before authentication
|
// Get selected role from local storage
|
||||||
final roleType = this.roleType.value;
|
final roleController = RoleSelectionController.instance;
|
||||||
final isOfficer = roleType == RoleType.officer;
|
final isOfficer = roleController.isOfficer.value;
|
||||||
|
final roleId = roleController.selectedRole.value!.id;
|
||||||
// Make sure we have a role selected
|
|
||||||
if (selectedRoleId.value.isEmpty) {
|
|
||||||
_updateSelectedRoleBasedOnType();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticate with Google
|
// Authenticate with Google
|
||||||
final authResponse =
|
final authResponse =
|
||||||
|
@ -320,7 +244,7 @@ class SignupWithRoleController extends GetxController {
|
||||||
// Create or update user metadata with role information
|
// Create or update user metadata with role information
|
||||||
final userMetadata = UserMetadataModel(
|
final userMetadata = UserMetadataModel(
|
||||||
isOfficer: isOfficer,
|
isOfficer: isOfficer,
|
||||||
roleId: selectedRoleId.value,
|
roleId: roleId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update user metadata in the database
|
// Update user metadata in the database
|
||||||
|
@ -372,14 +296,9 @@ class SignupWithRoleController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get selected role info before authentication
|
final roleController = RoleSelectionController.instance;
|
||||||
final roleType = this.roleType.value;
|
final isOfficer = roleController.isOfficer.value;
|
||||||
final isOfficer = roleType == RoleType.officer;
|
final roleId = roleController.selectedRole.value!.id;
|
||||||
|
|
||||||
// Make sure we have a role selected
|
|
||||||
if (selectedRoleId.value.isEmpty) {
|
|
||||||
_updateSelectedRoleBasedOnType();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticate with Apple
|
// Authenticate with Apple
|
||||||
final authResponse =
|
final authResponse =
|
||||||
|
@ -397,7 +316,7 @@ class SignupWithRoleController extends GetxController {
|
||||||
// Create or update user metadata with role information
|
// Create or update user metadata with role information
|
||||||
final userMetadata = UserMetadataModel(
|
final userMetadata = UserMetadataModel(
|
||||||
isOfficer: isOfficer,
|
isOfficer: isOfficer,
|
||||||
roleId: selectedRoleId.value,
|
roleId: roleId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update user metadata in the database
|
// Update user metadata in the database
|
||||||
|
@ -442,14 +361,10 @@ class SignupWithRoleController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get selected role info before authentication
|
// Get selected role from local storage
|
||||||
final roleType = this.roleType.value;
|
final roleController = RoleSelectionController.instance;
|
||||||
final isOfficer = roleType == RoleType.officer;
|
final isOfficer = roleController.isOfficer.value;
|
||||||
|
final roleId = roleController.selectedRole.value!.id;
|
||||||
// Make sure we have a role selected
|
|
||||||
if (selectedRoleId.value.isEmpty) {
|
|
||||||
_updateSelectedRoleBasedOnType();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticate with Facebook
|
// Authenticate with Facebook
|
||||||
final authResponse =
|
final authResponse =
|
||||||
|
@ -467,7 +382,7 @@ class SignupWithRoleController extends GetxController {
|
||||||
// Create or update user metadata with role information
|
// Create or update user metadata with role information
|
||||||
final userMetadata = UserMetadataModel(
|
final userMetadata = UserMetadataModel(
|
||||||
isOfficer: isOfficer,
|
isOfficer: isOfficer,
|
||||||
roleId: selectedRoleId.value,
|
roleId: roleId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update user metadata in the database
|
// Update user metadata in the database
|
||||||
|
@ -512,14 +427,10 @@ class SignupWithRoleController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get selected role info before authentication
|
// Get selected role from local storage
|
||||||
final roleType = this.roleType.value;
|
final roleController = RoleSelectionController.instance;
|
||||||
final isOfficer = roleType == RoleType.officer;
|
final isOfficer = roleController.isOfficer.value;
|
||||||
|
final roleId = roleController.selectedRole.value!.id;
|
||||||
// Make sure we have a role selected
|
|
||||||
if (selectedRoleId.value.isEmpty) {
|
|
||||||
_updateSelectedRoleBasedOnType();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authenticate with email and password
|
// Authenticate with email and password
|
||||||
final authResponse = await AuthenticationRepository.instance
|
final authResponse = await AuthenticationRepository.instance
|
||||||
|
@ -540,7 +451,7 @@ class SignupWithRoleController extends GetxController {
|
||||||
// Create or update user metadata with role information
|
// Create or update user metadata with role information
|
||||||
final userMetadata = UserMetadataModel(
|
final userMetadata = UserMetadataModel(
|
||||||
isOfficer: isOfficer,
|
isOfficer: isOfficer,
|
||||||
roleId: selectedRoleId.value,
|
roleId: roleId,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update user metadata in the database
|
// Update user metadata in the database
|
||||||
|
|
|
@ -13,10 +13,32 @@ final RxBool isFormValid = RxBool(true);
|
||||||
// Controllers
|
// Controllers
|
||||||
final nrpController = TextEditingController();
|
final nrpController = TextEditingController();
|
||||||
final rankController = TextEditingController();
|
final rankController = TextEditingController();
|
||||||
|
final unitIdController = TextEditingController();
|
||||||
|
final patrolUnitIdController = TextEditingController();
|
||||||
|
final nameController = TextEditingController();
|
||||||
|
final positionController = TextEditingController();
|
||||||
|
final phoneController = TextEditingController();
|
||||||
|
final emailController = TextEditingController();
|
||||||
|
final validUntilController = TextEditingController();
|
||||||
|
final avatarController = TextEditingController();
|
||||||
|
final qrCodeController = TextEditingController();
|
||||||
|
final bannedReasonController = TextEditingController();
|
||||||
|
final bannedUntilController = TextEditingController();
|
||||||
|
|
||||||
// Error states
|
// Error states
|
||||||
final RxString nrpError = ''.obs;
|
final RxString nrpError = ''.obs;
|
||||||
final RxString rankError = ''.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;
|
||||||
|
|
||||||
bool validate(GlobalKey<FormState> formKey) {
|
bool validate(GlobalKey<FormState> formKey) {
|
||||||
clearErrors();
|
clearErrors();
|
||||||
|
@ -47,18 +69,150 @@ final RxBool isFormValid = RxBool(true);
|
||||||
isFormValid.value = false;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
return isFormValid.value;
|
return isFormValid.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearErrors() {
|
void clearErrors() {
|
||||||
nrpError.value = '';
|
nrpError.value = '';
|
||||||
rankError.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 = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onClose() {
|
void onClose() {
|
||||||
nrpController.dispose();
|
nrpController.dispose();
|
||||||
rankController.dispose();
|
rankController.dispose();
|
||||||
|
unitIdController.dispose();
|
||||||
|
patrolUnitIdController.dispose();
|
||||||
|
nameController.dispose();
|
||||||
|
positionController.dispose();
|
||||||
|
phoneController.dispose();
|
||||||
|
emailController.dispose();
|
||||||
|
validUntilController.dispose();
|
||||||
|
avatarController.dispose();
|
||||||
|
qrCodeController.dispose();
|
||||||
|
bannedReasonController.dispose();
|
||||||
|
bannedUntilController.dispose();
|
||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,6 +81,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
AppBar _buildAppBar(BuildContext context, bool dark) {
|
AppBar _buildAppBar(BuildContext context, bool dark) {
|
||||||
return AppBar(
|
return AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
|
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Text(
|
title: Text(
|
||||||
|
@ -89,15 +90,7 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
context,
|
context,
|
||||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: true,
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.arrow_back,
|
|
||||||
color: dark ? TColors.white : TColors.black,
|
|
||||||
size: TSizes.iconMd,
|
|
||||||
),
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/auth_divider.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/password_field.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
|
import 'package:sigap/src/features/auth/presentasion/widgets/social_button.dart';
|
||||||
import 'package:sigap/src/shared/widgets/silver-app-bar/custom_silverbar.dart';
|
import 'package:sigap/src/features/onboarding/presentasion/controllers/role_selection_controller.dart';
|
||||||
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
import 'package:sigap/src/shared/widgets/text/custom_text_field.dart';
|
||||||
import 'package:sigap/src/utils/constants/colors.dart';
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
import 'package:sigap/src/utils/constants/image_strings.dart';
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
|
@ -19,52 +19,34 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Get the controller
|
|
||||||
final controller = Get.find<SignupWithRoleController>();
|
final controller = Get.find<SignupWithRoleController>();
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final isDark = THelperFunctions.isDarkMode(context);
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Obx(
|
body: SafeArea(
|
||||||
() => NestedScrollView(
|
child: SingleChildScrollView(
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
||||||
return [
|
child: Center(
|
||||||
// Top image section as SliverAppBar
|
|
||||||
_buildSliverAppBar(controller, context),
|
|
||||||
|
|
||||||
// Tab bar as pinned SliverPersistentHeader
|
|
||||||
SliverPersistentHeader(
|
|
||||||
delegate: TSliverTabBarDelegate(
|
|
||||||
child: _buildTabBar(context, controller),
|
|
||||||
minHeight: 70, // Height including padding
|
|
||||||
maxHeight: 70, // Fixed height for the tab bar
|
|
||||||
),
|
|
||||||
pinned: true,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
body: SafeArea(
|
|
||||||
top: false,
|
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
child: Column(
|
||||||
color: isDark ? TColors.dark : TColors.white,
|
mainAxisSize: MainAxisSize.min,
|
||||||
),
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
child: CustomScrollView(
|
children: [
|
||||||
slivers: [
|
// Logo di dalam container form
|
||||||
SliverPadding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
padding: const EdgeInsets.only(bottom: TSizes.lg),
|
||||||
sliver: SliverList(
|
child: Hero(
|
||||||
delegate: SliverChildListDelegate([
|
tag: 'app_logo',
|
||||||
_buildSignupForm(context, controller),
|
child: SvgPicture.asset(
|
||||||
]),
|
isDark ? TImages.darkAppBgLogo : TImages.lightAppBgLogo,
|
||||||
),
|
width: 100,
|
||||||
),
|
height: 100,
|
||||||
// Add extra padding at the bottom for safe area
|
fit: BoxFit.contain,
|
||||||
SliverToBoxAdapter(
|
),
|
||||||
child: SizedBox(
|
|
||||||
height: MediaQuery.of(context).padding.bottom,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
_buildSignupForm(context, controller),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -74,258 +56,12 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
SliverAppBar _buildSliverAppBar(
|
|
||||||
SignupWithRoleController controller,
|
|
||||||
BuildContext context,
|
|
||||||
) {
|
|
||||||
bool isOfficer = controller.roleType.value == RoleType.officer;
|
|
||||||
final isDark = THelperFunctions.isDarkMode(context);
|
|
||||||
final topPadding = MediaQuery.of(context).padding.top;
|
|
||||||
|
|
||||||
return SliverAppBar(
|
|
||||||
expandedHeight: MediaQuery.of(context).size.height * 0.35,
|
|
||||||
pinned: true,
|
|
||||||
backgroundColor: isDark ? TColors.dark : TColors.primary,
|
|
||||||
elevation: 0,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
flexibleSpace: FlexibleSpaceBar(
|
|
||||||
background: Stack(
|
|
||||||
children: [
|
|
||||||
// Background gradient with rounded bottom corners
|
|
||||||
Positioned.fill(
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
isDark ? Colors.black : TColors.primary,
|
|
||||||
isDark ? TColors.dark : TColors.primary.withOpacity(0.8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
bottomLeft: Radius.circular(30),
|
|
||||||
bottomRight: Radius.circular(30),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Role image
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.center,
|
|
||||||
child: LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
// Responsive image size based on available height/width
|
|
||||||
final double maxImageHeight = constraints.maxHeight * 0.9;
|
|
||||||
final double maxImageWidth = constraints.maxWidth * 0.9;
|
|
||||||
final double imageSize =
|
|
||||||
maxImageHeight < maxImageWidth
|
|
||||||
? maxImageHeight
|
|
||||||
: maxImageWidth;
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: imageSize,
|
|
||||||
width: imageSize,
|
|
||||||
child: SvgPicture.asset(
|
|
||||||
isOfficer
|
|
||||||
? (isDark
|
|
||||||
? TImages.communicationDark
|
|
||||||
: TImages.communication)
|
|
||||||
: (isDark ? TImages.fallingDark : TImages.falling),
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Back button with rounded container
|
|
||||||
leading: Padding(
|
|
||||||
padding: EdgeInsets.only(top: topPadding * 0.2),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Get.back(),
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(left: TSizes.md),
|
|
||||||
padding: const EdgeInsets.all(TSizes.xs),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.arrow_back, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Add rounded action button in top right corner
|
|
||||||
actions: [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(top: topPadding * 0.2, right: TSizes.md),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(TSizes.xs),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.2),
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.help_outline, color: Colors.white),
|
|
||||||
onPressed: () {
|
|
||||||
// Show help information
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder:
|
|
||||||
(context) => AlertDialog(
|
|
||||||
title: Text('Account Types'),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Viewer: Regular user account for general app access',
|
|
||||||
),
|
|
||||||
SizedBox(height: TSizes.sm),
|
|
||||||
Text(
|
|
||||||
'Officer: Security personnel account with additional features and permissions',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
child: Text('Got it'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTabBar(
|
|
||||||
BuildContext context,
|
|
||||||
SignupWithRoleController controller,
|
|
||||||
) {
|
|
||||||
final isDark = THelperFunctions.isDarkMode(context);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isDark ? TColors.dark : TColors.lightContainer,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 5,
|
|
||||||
offset: const Offset(0, 3),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft: Radius.circular(30),
|
|
||||||
topRight: Radius.circular(30),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.fromLTRB(
|
|
||||||
TSizes.defaultSpace,
|
|
||||||
TSizes.xs,
|
|
||||||
TSizes.defaultSpace,
|
|
||||||
TSizes.xs,
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
height: 60,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isDark ? TColors.dark : TColors.lightContainer,
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// Viewer Tab
|
|
||||||
_buildTab(
|
|
||||||
context: context,
|
|
||||||
controller: controller,
|
|
||||||
roleType: RoleType.viewer,
|
|
||||||
label: 'Viewer',
|
|
||||||
icon: Icons.person,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Officer Tab
|
|
||||||
_buildTab(
|
|
||||||
context: context,
|
|
||||||
controller: controller,
|
|
||||||
roleType: RoleType.officer,
|
|
||||||
label: 'Officer',
|
|
||||||
icon: Icons.security,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTab({
|
|
||||||
required BuildContext context,
|
|
||||||
required SignupWithRoleController controller,
|
|
||||||
required RoleType roleType,
|
|
||||||
required String label,
|
|
||||||
required IconData icon,
|
|
||||||
}) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
bool isSelected = controller.roleType.value == roleType;
|
|
||||||
Color selectedColor =
|
|
||||||
roleType == RoleType.viewer ? TColors.primary : TColors.primary;
|
|
||||||
|
|
||||||
return Expanded(
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => controller.setRoleType(roleType),
|
|
||||||
child: AnimatedContainer(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected ? selectedColor : Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusLg),
|
|
||||||
),
|
|
||||||
// Add padding to make content larger
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
color:
|
|
||||||
isSelected
|
|
||||||
? Colors.white
|
|
||||||
: theme.colorScheme.onSurfaceVariant,
|
|
||||||
// Increase icon size from 18 to 22 or 24
|
|
||||||
size: 22,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 10), // Increased from 8
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
|
||||||
color:
|
|
||||||
isSelected
|
|
||||||
? Colors.white
|
|
||||||
: theme.colorScheme.onSurfaceVariant,
|
|
||||||
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
|
|
||||||
// Increase text size
|
|
||||||
fontSize: 16, // Add explicit font size
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSignupForm(
|
Widget _buildSignupForm(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
SignupWithRoleController controller,
|
SignupWithRoleController controller,
|
||||||
) {
|
) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
bool isOfficer = controller.roleType.value == RoleType.officer;
|
|
||||||
Color themeColor = isOfficer ? TColors.primary : TColors.primary;
|
|
||||||
final isDark = THelperFunctions.isDarkMode(context);
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
|
@ -336,15 +72,12 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
Text(
|
Text(
|
||||||
'Create Your Account',
|
'Create Your Account',
|
||||||
style: theme.textTheme.headlineMedium?.copyWith(
|
style: theme.textTheme.headlineMedium?.copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
color: isDark ? TColors.light : TColors.dark,
|
||||||
color: themeColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.sm),
|
const SizedBox(height: TSizes.sm),
|
||||||
Text(
|
Text(
|
||||||
isOfficer
|
'Please fill in the details below to create your account',
|
||||||
? 'Sign up as a security officer to access all features'
|
|
||||||
: 'Sign up as a viewer to explore the application',
|
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
color: isDark ? TColors.textSecondary : Colors.grey.shade600,
|
color: isDark ? TColors.textSecondary : Colors.grey.shade600,
|
||||||
),
|
),
|
||||||
|
@ -352,7 +85,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
const SizedBox(height: TSizes.spaceBtwSections),
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
||||||
// Social Login Buttons
|
// Social Login Buttons
|
||||||
_buildSocialLoginButtons(controller, themeColor, isDark),
|
_buildSocialLoginButtons(controller, isDark),
|
||||||
|
|
||||||
// Or divider
|
// Or divider
|
||||||
const AuthDivider(text: 'OR'),
|
const AuthDivider(text: 'OR'),
|
||||||
|
@ -370,7 +103,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
prefixIcon: const Icon(Icons.email_outlined),
|
prefixIcon: const Icon(Icons.email_outlined),
|
||||||
hintText: 'Enter your email',
|
hintText: 'Enter your email',
|
||||||
accentColor: themeColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -386,7 +118,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
textInputAction: TextInputAction.next,
|
textInputAction: TextInputAction.next,
|
||||||
prefixIcon: const Icon(Icons.lock_outlined),
|
prefixIcon: const Icon(Icons.lock_outlined),
|
||||||
hintText: 'Enter your password',
|
hintText: 'Enter your password',
|
||||||
accentColor: themeColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -402,7 +133,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
prefixIcon: const Icon(Icons.lock_outlined),
|
prefixIcon: const Icon(Icons.lock_outlined),
|
||||||
hintText: 'Confirm your password',
|
hintText: 'Confirm your password',
|
||||||
accentColor: themeColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -413,9 +143,6 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
fillColor: WidgetStateProperty.resolveWith<Color>((
|
fillColor: WidgetStateProperty.resolveWith<Color>((
|
||||||
Set<WidgetState> states,
|
Set<WidgetState> states,
|
||||||
) {
|
) {
|
||||||
if (states.contains(WidgetState.selected)) {
|
|
||||||
return themeColor;
|
|
||||||
}
|
|
||||||
return isDark ? Colors.grey.shade700 : Colors.grey.shade300;
|
return isDark ? Colors.grey.shade700 : Colors.grey.shade300;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
|
@ -448,13 +175,12 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
Obx(
|
Obx(
|
||||||
() => AuthButton(
|
() => AuthButton(
|
||||||
text: 'Sign Up',
|
text: 'Sign Up',
|
||||||
onPressed: () => controller.signUp(isOfficer),
|
onPressed: () => controller.signUp(),
|
||||||
isLoading: controller.isLoading.value,
|
isLoading: controller.isLoading.value,
|
||||||
backgroundColor: themeColor,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwInputFields / 2),
|
||||||
|
|
||||||
// Already have an account row
|
// Already have an account row
|
||||||
Row(
|
Row(
|
||||||
|
@ -468,11 +194,21 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: controller.goToSignIn,
|
onPressed: controller.goToSignIn,
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
overlayColor: TColors.transparent,
|
||||||
|
foregroundColor:
|
||||||
|
isDark
|
||||||
|
? TColors.light.withOpacity(0.8)
|
||||||
|
: TColors.primary.withOpacity(0.8),
|
||||||
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Sign In',
|
'Sign In',
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
color: themeColor,
|
color:
|
||||||
fontWeight: FontWeight.w500,
|
isDark
|
||||||
|
? TColors.light.withOpacity(0.8)
|
||||||
|
: TColors.primary.withOpacity(0.8),
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -488,7 +224,7 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
|
|
||||||
Widget _buildSocialLoginButtons(
|
Widget _buildSocialLoginButtons(
|
||||||
SignupWithRoleController controller,
|
SignupWithRoleController controller,
|
||||||
Color themeColor,
|
|
||||||
bool isDark,
|
bool isDark,
|
||||||
) {
|
) {
|
||||||
return Column(
|
return Column(
|
||||||
|
|
|
@ -29,6 +29,44 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwItems),
|
const SizedBox(height: TSizes.spaceBtwItems),
|
||||||
|
|
||||||
|
// Unit ID field
|
||||||
|
Obx(
|
||||||
|
() => CustomTextField(
|
||||||
|
label: 'Unit ID',
|
||||||
|
controller: controller.unitIdController,
|
||||||
|
validator: (v) => TValidators.validateUserInput('Unit ID', v, 20),
|
||||||
|
errorText: controller.unitIdError.value,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
hintText: 'e.g., POLRES01',
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.unitIdController.text = value;
|
||||||
|
controller.unitIdError.value = '';
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Patrol Unit ID field
|
||||||
|
Obx(
|
||||||
|
() => CustomTextField(
|
||||||
|
label: 'Patrol Unit ID',
|
||||||
|
controller: controller.patrolUnitIdController,
|
||||||
|
validator:
|
||||||
|
(v) => TValidators.validateUserInput(
|
||||||
|
'Patrol Unit ID',
|
||||||
|
v,
|
||||||
|
100,
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
|
errorText: controller.patrolUnitIdError.value,
|
||||||
|
textInputAction: TextInputAction.next,
|
||||||
|
hintText: 'e.g., PATROL01',
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.patrolUnitIdController.text = value;
|
||||||
|
controller.patrolUnitIdError.value = '';
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// NRP field
|
// NRP field
|
||||||
Obx(
|
Obx(
|
||||||
() => CustomTextField(
|
() => CustomTextField(
|
||||||
|
@ -53,7 +91,7 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
controller: controller.rankController,
|
controller: controller.rankController,
|
||||||
validator: TValidators.validateRank,
|
validator: TValidators.validateRank,
|
||||||
errorText: controller.rankError.value,
|
errorText: controller.rankError.value,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.next,
|
||||||
hintText: 'e.g., Captain',
|
hintText: 'e.g., Captain',
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
controller.rankController.text = value;
|
controller.rankController.text = value;
|
||||||
|
@ -61,6 +99,28 @@ class OfficerInfoStep extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Position field
|
||||||
|
Obx(
|
||||||
|
() => CustomTextField(
|
||||||
|
label: 'Position',
|
||||||
|
controller: controller.positionController,
|
||||||
|
validator:
|
||||||
|
(v) => TValidators.validateUserInput(
|
||||||
|
'Position',
|
||||||
|
v,
|
||||||
|
100,
|
||||||
|
required: true,
|
||||||
|
),
|
||||||
|
errorText: controller.positionError.value,
|
||||||
|
textInputAction: TextInputAction.done,
|
||||||
|
hintText: 'e.g., Head of Unit',
|
||||||
|
onChanged: (value) {
|
||||||
|
controller.positionController.text = value;
|
||||||
|
controller.positionError.value = '';
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,7 +21,7 @@ class AuthButton extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 50,
|
height: 55,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: isLoading ? null : onPressed,
|
onPressed: isLoading ? null : onPressed,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.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/repositories/roles_repository.dart';
|
import 'package:sigap/src/features/personalization/data/repositories/roles_repository.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
||||||
|
enum RoleType { viewer, officer }
|
||||||
|
|
||||||
class RoleSelectionController extends GetxController {
|
class RoleSelectionController extends GetxController {
|
||||||
static RoleSelectionController get instance => Get.find();
|
static RoleSelectionController get instance => Get.find();
|
||||||
|
|
||||||
|
@ -16,6 +17,9 @@ class RoleSelectionController extends GetxController {
|
||||||
// Selected role
|
// Selected role
|
||||||
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
|
final Rx<RoleModel?> selectedRole = Rx<RoleModel?>(null);
|
||||||
|
|
||||||
|
// Role type (Viewer or Officer)
|
||||||
|
final Rx<RoleType> roleType = RoleType.viewer.obs;
|
||||||
|
|
||||||
// Loading state
|
// Loading state
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final RxBool isOfficer = false.obs;
|
final RxBool isOfficer = false.obs;
|
||||||
|
@ -28,7 +32,6 @@ class RoleSelectionController extends GetxController {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
fetchRoles();
|
fetchRoles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Fetch available roles from repository
|
// Fetch available roles from repository
|
||||||
Future<void> fetchRoles() async {
|
Future<void> fetchRoles() async {
|
||||||
|
@ -62,44 +65,53 @@ class RoleSelectionController extends GetxController {
|
||||||
// Select a role
|
// Select a role
|
||||||
void selectRole(RoleModel role) {
|
void selectRole(RoleModel role) {
|
||||||
selectedRole.value = role;
|
selectedRole.value = role;
|
||||||
|
|
||||||
|
// Set role type based on selected role
|
||||||
|
if (role.name.toLowerCase() == 'officer') {
|
||||||
|
roleType.value = RoleType.officer;
|
||||||
|
isOfficer.value = true;
|
||||||
|
} else {
|
||||||
|
roleType.value = RoleType.viewer;
|
||||||
|
isOfficer.value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with selected role
|
// Continue with selected role
|
||||||
Future<void> continueWithRole() async {
|
// Future<void> continueWithRole() async {
|
||||||
if (selectedRole.value == null) {
|
// if (selectedRole.value == null) {
|
||||||
TLoaders.errorSnackBar(
|
// TLoaders.errorSnackBar(
|
||||||
title: 'Error',
|
// title: 'Error',
|
||||||
message: 'Please select a role to continue.',
|
// message: 'Please select a role to continue.',
|
||||||
);
|
// );
|
||||||
return;
|
// return;
|
||||||
}
|
// }
|
||||||
|
|
||||||
try {
|
// try {
|
||||||
isLoading.value = true;
|
// isLoading.value = true;
|
||||||
|
|
||||||
// Logger().i('Selected role: ${selectedRole.value?.name}');
|
// // Logger().i('Selected role: ${selectedRole.value?.name}');
|
||||||
|
|
||||||
// Check if the selected role is officer
|
// // Check if the selected role is officer
|
||||||
if (selectedRole.value?.name.toLowerCase() == 'officer') {
|
// if (selectedRole.value?.name.toLowerCase() == 'officer') {
|
||||||
isOfficer.value = true;
|
// isOfficer.value = true;
|
||||||
} else {
|
// } else {
|
||||||
isOfficer.value = false;
|
// isOfficer.value = false;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Navigate directly to step form with selected role
|
// // Navigate directly to step form with selected role
|
||||||
Get.toNamed(
|
// Get.toNamed(
|
||||||
AppRoutes.registrationForm,
|
// AppRoutes.registrationForm,
|
||||||
arguments: {'role': selectedRole.value},
|
// arguments: {'role': selectedRole.value},
|
||||||
);
|
// );
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
TLoaders.errorSnackBar(
|
// TLoaders.errorSnackBar(
|
||||||
title: 'Error',
|
// title: 'Error',
|
||||||
message:
|
// message:
|
||||||
'An error occurred while selecting the role. Please try again.',
|
// 'An error occurred while selecting the role. Please try again.',
|
||||||
);
|
// );
|
||||||
// Logger().e('Error in continueWithRole: $e');
|
// // Logger().e('Error in continueWithRole: $e');
|
||||||
} finally {
|
// } finally {
|
||||||
isLoading.value = false;
|
// isLoading.value = false;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,8 @@ import 'package:sigap/src/utils/helpers/helper_functions.dart';
|
||||||
import 'package:sigap/src/utils/loaders/shimmer.dart';
|
import 'package:sigap/src/utils/loaders/shimmer.dart';
|
||||||
|
|
||||||
class RoleSelectionScreen extends StatelessWidget {
|
class RoleSelectionScreen extends StatelessWidget {
|
||||||
const RoleSelectionScreen({super.key});
|
final void Function(String roleId)? onRoleSelected;
|
||||||
|
const RoleSelectionScreen({super.key, this.onRoleSelected});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -103,61 +104,10 @@ class RoleSelectionScreen extends StatelessWidget {
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 48),
|
const SizedBox(height: 48),
|
||||||
Obx(
|
Obx(
|
||||||
() => SizedBox(
|
() =>
|
||||||
width: double.infinity,
|
controller.selectedRole.value != null
|
||||||
height: 48,
|
? _SwipeRightHint(isDark: isDark)
|
||||||
child: ElevatedButton(
|
: const SizedBox(height: 48),
|
||||||
onPressed:
|
|
||||||
controller.selectedRole.value != null
|
|
||||||
? controller.continueWithRole
|
|
||||||
: null,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor:
|
|
||||||
isDark
|
|
||||||
? Colors.white
|
|
||||||
: const Color(0xFF2F2F2F),
|
|
||||||
foregroundColor:
|
|
||||||
isDark ? Colors.black : Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
elevation: 0,
|
|
||||||
disabledBackgroundColor:
|
|
||||||
isDark
|
|
||||||
? const Color(0xFF343536)
|
|
||||||
: const Color(0xFFF1F1F1),
|
|
||||||
disabledForegroundColor: const Color(0xFFB0B0B0),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
vertical: 0,
|
|
||||||
horizontal: 0,
|
|
||||||
), // reset padding
|
|
||||||
minimumSize: const Size(0, 48), // ensure height
|
|
||||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
|
||||||
),
|
|
||||||
child:
|
|
||||||
controller.isLoading.value
|
|
||||||
? SizedBox(
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
color:
|
|
||||||
isDark
|
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
strokeWidth: 2,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: FittedBox(
|
|
||||||
fit: BoxFit.scaleDown,
|
|
||||||
child: Text(
|
|
||||||
'Get started',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: TSizes.fontSizeMd,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
letterSpacing: 0.1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -486,3 +436,74 @@ class RoleSelectionScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Tambahkan widget animasi swipe hint di bawah:
|
||||||
|
class _SwipeRightHint extends StatefulWidget {
|
||||||
|
final bool isDark;
|
||||||
|
const _SwipeRightHint({required this.isDark});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_SwipeRightHint> createState() => _SwipeRightHintState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SwipeRightHintState extends State<_SwipeRightHint>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late AnimationController _controller;
|
||||||
|
late Animation<double> _animation;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = AnimationController(
|
||||||
|
vsync: this,
|
||||||
|
duration: const Duration(milliseconds: 1200),
|
||||||
|
)..repeat(reverse: true);
|
||||||
|
_animation = Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: 32,
|
||||||
|
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = widget.isDark ? Colors.white : const Color(0xFF2F2F2F);
|
||||||
|
return SizedBox(
|
||||||
|
height: 48,
|
||||||
|
child: Center(
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _animation,
|
||||||
|
builder:
|
||||||
|
(context, child) => Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Swipe right to register',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TSizes.fontSizeMd,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Transform.translate(
|
||||||
|
offset: Offset(_animation.value, 0),
|
||||||
|
child: Icon(
|
||||||
|
Icons.arrow_forward_rounded,
|
||||||
|
color: color,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart';
|
||||||
|
import 'package:sigap/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/colors.dart';
|
||||||
|
|
||||||
|
class RoleSignupPageView extends StatefulWidget {
|
||||||
|
const RoleSignupPageView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RoleSignupPageView> createState() => _RoleSignupPageViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RoleSignupPageViewState extends State<RoleSignupPageView> {
|
||||||
|
final PageController _pageController = PageController();
|
||||||
|
int _currentPage = 0;
|
||||||
|
String? _selectedRoleId;
|
||||||
|
|
||||||
|
void _onRoleSelected(String roleId) {
|
||||||
|
setState(() {
|
||||||
|
_selectedRoleId = roleId;
|
||||||
|
_currentPage = 1;
|
||||||
|
});
|
||||||
|
_pageController.animateToPage(
|
||||||
|
1,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.ease,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Stack(
|
||||||
|
children: [
|
||||||
|
PageView(
|
||||||
|
controller: _pageController,
|
||||||
|
physics: const ClampingScrollPhysics(),
|
||||||
|
onPageChanged: (index) {
|
||||||
|
setState(() {
|
||||||
|
_currentPage = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
RoleSelectionScreen(onRoleSelected: _onRoleSelected),
|
||||||
|
SignupWithRoleScreen(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: MediaQuery.of(context).padding.top + 16,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: List.generate(
|
||||||
|
2,
|
||||||
|
(index) => AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
width: _currentPage == index ? 12 : 8,
|
||||||
|
height: _currentPage == index ? 12 : 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
_currentPage == index
|
||||||
|
? TColors.primary
|
||||||
|
: Colors.grey[400],
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,7 +87,7 @@ class WelcomeScreen extends StatelessWidget {
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: controller.getStarted,
|
onPressed: controller.getStarted,
|
||||||
style: theme.elevatedButtonTheme.style,
|
style: theme.elevatedButtonTheme.style,
|
||||||
child: Text('Get Started'),
|
child: Text('Perform Location Check'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl),
|
SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl),
|
||||||
|
|
|
@ -35,7 +35,7 @@ class TAppTheme {
|
||||||
fontFamily: 'Poppins',
|
fontFamily: 'Poppins',
|
||||||
disabledColor: TColors.grey,
|
disabledColor: TColors.grey,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
primaryColor: TColors.primary,
|
primaryColor: TColors.light,
|
||||||
textTheme: TTextTheme.darkTextTheme,
|
textTheme: TTextTheme.darkTextTheme,
|
||||||
chipTheme: TChipTheme.darkChipTheme,
|
chipTheme: TChipTheme.darkChipTheme,
|
||||||
scaffoldBackgroundColor: TColors.black,
|
scaffoldBackgroundColor: TColors.black,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../constants/colors.dart';
|
import '../../constants/colors.dart';
|
||||||
|
|
||||||
/// Custom Class for Light & Dark Text Themes
|
/// Custom Class for Light & Dark Text Themes
|
||||||
|
|
Loading…
Reference in New Issue