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:
vergiLgood1 2025-05-26 03:39:54 +07:00
parent 6a4813c15e
commit dd2bb57e42
14 changed files with 497 additions and 532 deletions

View File

@ -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(

View File

@ -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);
} }
} }
} }

View File

@ -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

View File

@ -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();
} }
} }

View File

@ -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(),
),
); );
} }

View File

@ -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,300 +19,37 @@ 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(
() => NestedScrollView(
headerSliverBuilder: (context, innerBoxIsScrolled) {
return [
// 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( body: SafeArea(
top: false, child: SingleChildScrollView(
child: Container(
decoration: BoxDecoration(
color: isDark ? TColors.dark : TColors.white,
),
child: CustomScrollView(
slivers: [
SliverPadding(
padding: const EdgeInsets.all(TSizes.defaultSpace), padding: const EdgeInsets.all(TSizes.defaultSpace),
sliver: SliverList( child: Center(
delegate: SliverChildListDelegate([
_buildSignupForm(context, controller),
]),
),
),
// Add extra padding at the bottom for safe area
SliverToBoxAdapter(
child: SizedBox(
height: MediaQuery.of(context).padding.bottom,
),
),
],
),
),
),
),
),
);
}
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( child: Container(
decoration: BoxDecoration( child: Column(
gradient: LinearGradient( mainAxisSize: MainAxisSize.min,
begin: Alignment.topCenter, crossAxisAlignment: CrossAxisAlignment.center,
end: Alignment.bottomCenter, children: [
colors: [ // Logo di dalam container form
isDark ? Colors.black : TColors.primary, Padding(
isDark ? TColors.dark : TColors.primary.withOpacity(0.8), padding: const EdgeInsets.only(bottom: TSizes.lg),
], child: Hero(
), tag: 'app_logo',
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( child: SvgPicture.asset(
isOfficer isDark ? TImages.darkAppBgLogo : TImages.lightAppBgLogo,
? (isDark width: 100,
? TImages.communicationDark height: 100,
: TImages.communication)
: (isDark ? TImages.fallingDark : TImages.falling),
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
);
},
), ),
), ),
_buildSignupForm(context, controller),
], ],
), ),
), ),
// 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
),
),
],
), ),
), ),
), ),
@ -324,8 +61,7 @@ class SignupWithRoleScreen extends StatelessWidget {
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(

View File

@ -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 = '';
},
),
),
], ],
), ),
); );

View File

@ -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(

View File

@ -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;
@ -29,7 +33,6 @@ class RoleSelectionController extends GetxController {
fetchRoles(); fetchRoles();
} }
// Fetch available roles from repository // Fetch available roles from repository
Future<void> fetchRoles() async { Future<void> fetchRoles() async {
try { try {
@ -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;
} // }
} // }
} }

View File

@ -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,
height: 48,
child: ElevatedButton(
onPressed:
controller.selectedRole.value != null controller.selectedRole.value != null
? controller.continueWithRole ? _SwipeRightHint(isDark: isDark)
: null, : const SizedBox(height: 48),
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,
),
),
],
),
),
),
);
}
}

View File

@ -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,
),
),
),
),
),
],
),
);
}
}

View File

@ -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),

View File

@ -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,

View File

@ -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