diff --git a/sigap-mobile/lib/src/cores/routes/app_pages.dart b/sigap-mobile/lib/src/cores/routes/app_pages.dart index dbae65b..6f1039c 100644 --- a/sigap-mobile/lib/src/cores/routes/app_pages.dart +++ b/sigap-mobile/lib/src/cores/routes/app_pages.dart @@ -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/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_signup_pageview.dart'; import 'package:sigap/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart'; import 'package:sigap/src/utils/constants/app_routes.dart'; @@ -25,7 +26,7 @@ class AppPages { // Auth GetPage( name: AppRoutes.roleSelection, - page: () => const RoleSelectionScreen(), + page: () => const RoleSignupPageView(), ), GetPage( diff --git a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart index 4769278..92bef55 100644 --- a/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart +++ b/sigap-mobile/lib/src/features/auth/data/repositories/authentication_repository.dart @@ -140,7 +140,7 @@ class AuthenticationRepository extends GetxController { if (isFirstTime) { _navigateToRoute(AppRoutes.onboarding); } else { - _navigateToRoute(AppRoutes.onboarding); + _navigateToRoute(AppRoutes.signIn); } } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/main/signup_with_role_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/main/signup_with_role_controller.dart index 2e48ce8..3083720 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/main/signup_with_role_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/main/signup_with_role_controller.dart @@ -5,6 +5,7 @@ import 'package:get/get.dart'; import 'package:get_storage/get_storage.dart'; import 'package:logger/logger.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/models/user_metadata_model.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) final RxBool isAppleSignInAvailable = RxBool(Platform.isIOS); - @override - void onInit() { - super.onInit(); - loadRoles(); - } - @override void onClose() { emailController.dispose(); @@ -67,55 +62,6 @@ class SignupWithRoleController extends GetxController { super.onClose(); } - // Load available roles - Future 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 String? validateEmail(String? value) { final error = TValidators.validateEmail(value); @@ -168,7 +114,7 @@ class SignupWithRoleController extends GetxController { // Sign up function /// Updated signup function with better error handling and argument passing - void signUp(bool isOfficer) async { + void signUp() async { try { isLoading.value = true; Logger().i('SignUp process started'); @@ -196,13 +142,16 @@ class SignupWithRoleController extends GetxController { return; } - // Ensure we have a role selected - if (selectedRoleId.value.isEmpty) { - _updateSelectedRoleBasedOnType(); - } + // Get selected role info before authentication + + final roleController = RoleSelectionController.instance; + final isOfficer = roleController.isOfficer.value; + final roleId = roleController.selectedRole.value!.id; + + Logger().i('Is officer: $isOfficer'); // Validate role selection - if (selectedRoleId.value.isEmpty) { + if (roleId.isEmpty) { TLoaders.errorSnackBar( title: 'Role Required', message: 'Please select a role before continuing.', @@ -213,11 +162,13 @@ class SignupWithRoleController extends GetxController { // Create comprehensive initial user metadata final initialMetadata = UserMetadataModel( email: emailController.text.trim(), - roleId: selectedRoleId.value, + roleId: roleId, isOfficer: isOfficer, profileStatus: 'incomplete', ); + Logger().i('Initial metadata: ${initialMetadata.toJson()}'); + // Create the account final authResponse = await AuthenticationRepository.instance .initialSignUp( @@ -238,11 +189,6 @@ class SignupWithRoleController extends GetxController { final user = authResponse.user!; 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(); } catch (e) { Logger().e('Error during signup: $e'); @@ -264,24 +210,6 @@ class SignupWithRoleController extends GetxController { } } - /// Store temporary data for the verification process - Future _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 Future signInWithGoogle() async { try { @@ -297,14 +225,10 @@ class SignupWithRoleController extends GetxController { return; } - // Get selected role info before authentication - final roleType = this.roleType.value; - final isOfficer = roleType == RoleType.officer; - - // Make sure we have a role selected - if (selectedRoleId.value.isEmpty) { - _updateSelectedRoleBasedOnType(); - } + // Get selected role from local storage + final roleController = RoleSelectionController.instance; + final isOfficer = roleController.isOfficer.value; + final roleId = roleController.selectedRole.value!.id; // Authenticate with Google final authResponse = @@ -320,7 +244,7 @@ class SignupWithRoleController extends GetxController { // Create or update user metadata with role information final userMetadata = UserMetadataModel( isOfficer: isOfficer, - roleId: selectedRoleId.value, + roleId: roleId, ); // Update user metadata in the database @@ -372,14 +296,9 @@ class SignupWithRoleController extends GetxController { return; } - // Get selected role info before authentication - final roleType = this.roleType.value; - final isOfficer = roleType == RoleType.officer; - - // Make sure we have a role selected - if (selectedRoleId.value.isEmpty) { - _updateSelectedRoleBasedOnType(); - } + final roleController = RoleSelectionController.instance; + final isOfficer = roleController.isOfficer.value; + final roleId = roleController.selectedRole.value!.id; // Authenticate with Apple final authResponse = @@ -397,7 +316,7 @@ class SignupWithRoleController extends GetxController { // Create or update user metadata with role information final userMetadata = UserMetadataModel( isOfficer: isOfficer, - roleId: selectedRoleId.value, + roleId: roleId, ); // Update user metadata in the database @@ -442,14 +361,10 @@ class SignupWithRoleController extends GetxController { return; } - // Get selected role info before authentication - final roleType = this.roleType.value; - final isOfficer = roleType == RoleType.officer; - - // Make sure we have a role selected - if (selectedRoleId.value.isEmpty) { - _updateSelectedRoleBasedOnType(); - } + // Get selected role from local storage + final roleController = RoleSelectionController.instance; + final isOfficer = roleController.isOfficer.value; + final roleId = roleController.selectedRole.value!.id; // Authenticate with Facebook final authResponse = @@ -467,7 +382,7 @@ class SignupWithRoleController extends GetxController { // Create or update user metadata with role information final userMetadata = UserMetadataModel( isOfficer: isOfficer, - roleId: selectedRoleId.value, + roleId: roleId, ); // Update user metadata in the database @@ -512,14 +427,10 @@ class SignupWithRoleController extends GetxController { return; } - // Get selected role info before authentication - final roleType = this.roleType.value; - final isOfficer = roleType == RoleType.officer; - - // Make sure we have a role selected - if (selectedRoleId.value.isEmpty) { - _updateSelectedRoleBasedOnType(); - } + // Get selected role from local storage + final roleController = RoleSelectionController.instance; + final isOfficer = roleController.isOfficer.value; + final roleId = roleController.selectedRole.value!.id; // Authenticate with email and password final authResponse = await AuthenticationRepository.instance @@ -540,7 +451,7 @@ class SignupWithRoleController extends GetxController { // Create or update user metadata with role information final userMetadata = UserMetadataModel( isOfficer: isOfficer, - roleId: selectedRoleId.value, + roleId: roleId, ); // Update user metadata in the database diff --git a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart index 112c6aa..c38f355 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/controllers/signup/step/officer-information/officer_info_controller.dart @@ -13,10 +13,32 @@ final RxBool isFormValid = RxBool(true); // Controllers final nrpController = 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 final RxString nrpError = ''.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 formKey) { clearErrors(); @@ -47,18 +69,150 @@ final RxBool isFormValid = RxBool(true); 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; } void clearErrors() { nrpError.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 void onClose() { nrpController.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(); } } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart index acb67d7..58a43d6 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/registraion_form_screen.dart @@ -81,6 +81,7 @@ class FormRegistrationScreen extends StatelessWidget { AppBar _buildAppBar(BuildContext context, bool dark) { return AppBar( automaticallyImplyLeading: false, + backgroundColor: Colors.transparent, elevation: 0, title: Text( @@ -89,15 +90,7 @@ class FormRegistrationScreen extends StatelessWidget { context, ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), ), - centerTitle: false, - leading: IconButton( - icon: Icon( - Icons.arrow_back, - color: dark ? TColors.white : TColors.black, - size: TSizes.iconMd, - ), - onPressed: () => Get.back(), - ), + centerTitle: true, ); } diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart index 3a90461..b7e29bf 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/main/signup_with_role_screen.dart @@ -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/password_field.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/utils/constants/colors.dart'; import 'package:sigap/src/utils/constants/image_strings.dart'; @@ -19,52 +19,34 @@ class SignupWithRoleScreen extends StatelessWidget { @override Widget build(BuildContext context) { - // Get the controller final controller = Get.find(); final theme = Theme.of(context); final isDark = THelperFunctions.isDarkMode(context); 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( - top: false, + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(TSizes.defaultSpace), + child: Center( child: Container( - decoration: BoxDecoration( - color: isDark ? TColors.dark : TColors.white, - ), - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.all(TSizes.defaultSpace), - sliver: SliverList( - delegate: SliverChildListDelegate([ - _buildSignupForm(context, controller), - ]), - ), - ), - // Add extra padding at the bottom for safe area - SliverToBoxAdapter( - child: SizedBox( - height: MediaQuery.of(context).padding.bottom, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Logo di dalam container form + Padding( + padding: const EdgeInsets.only(bottom: TSizes.lg), + child: Hero( + tag: 'app_logo', + child: SvgPicture.asset( + isDark ? TImages.darkAppBgLogo : TImages.lightAppBgLogo, + width: 100, + height: 100, + fit: BoxFit.contain, + ), ), ), + _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( BuildContext context, SignupWithRoleController controller, ) { final theme = Theme.of(context); - bool isOfficer = controller.roleType.value == RoleType.officer; - Color themeColor = isOfficer ? TColors.primary : TColors.primary; + final isDark = THelperFunctions.isDarkMode(context); return Form( @@ -336,15 +72,12 @@ class SignupWithRoleScreen extends StatelessWidget { Text( 'Create Your Account', style: theme.textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - color: themeColor, + color: isDark ? TColors.light : TColors.dark, ), ), const SizedBox(height: TSizes.sm), Text( - isOfficer - ? 'Sign up as a security officer to access all features' - : 'Sign up as a viewer to explore the application', + 'Please fill in the details below to create your account', style: theme.textTheme.bodyMedium?.copyWith( color: isDark ? TColors.textSecondary : Colors.grey.shade600, ), @@ -352,7 +85,7 @@ class SignupWithRoleScreen extends StatelessWidget { const SizedBox(height: TSizes.spaceBtwSections), // Social Login Buttons - _buildSocialLoginButtons(controller, themeColor, isDark), + _buildSocialLoginButtons(controller, isDark), // Or divider const AuthDivider(text: 'OR'), @@ -370,7 +103,6 @@ class SignupWithRoleScreen extends StatelessWidget { textInputAction: TextInputAction.next, prefixIcon: const Icon(Icons.email_outlined), hintText: 'Enter your email', - accentColor: themeColor, ), ), @@ -386,7 +118,6 @@ class SignupWithRoleScreen extends StatelessWidget { textInputAction: TextInputAction.next, prefixIcon: const Icon(Icons.lock_outlined), hintText: 'Enter your password', - accentColor: themeColor, ), ), @@ -402,7 +133,6 @@ class SignupWithRoleScreen extends StatelessWidget { textInputAction: TextInputAction.done, prefixIcon: const Icon(Icons.lock_outlined), hintText: 'Confirm your password', - accentColor: themeColor, ), ), @@ -413,9 +143,6 @@ class SignupWithRoleScreen extends StatelessWidget { fillColor: WidgetStateProperty.resolveWith(( Set states, ) { - if (states.contains(WidgetState.selected)) { - return themeColor; - } return isDark ? Colors.grey.shade700 : Colors.grey.shade300; }), ), @@ -448,13 +175,12 @@ class SignupWithRoleScreen extends StatelessWidget { Obx( () => AuthButton( text: 'Sign Up', - onPressed: () => controller.signUp(isOfficer), + onPressed: () => controller.signUp(), isLoading: controller.isLoading.value, - backgroundColor: themeColor, ), ), - const SizedBox(height: TSizes.spaceBtwItems), + const SizedBox(height: TSizes.spaceBtwInputFields / 2), // Already have an account row Row( @@ -468,11 +194,21 @@ class SignupWithRoleScreen extends StatelessWidget { ), TextButton( onPressed: controller.goToSignIn, + style: TextButton.styleFrom( + overlayColor: TColors.transparent, + foregroundColor: + isDark + ? TColors.light.withOpacity(0.8) + : TColors.primary.withOpacity(0.8), + ), child: Text( 'Sign In', - style: theme.textTheme.bodyMedium?.copyWith( - color: themeColor, - fontWeight: FontWeight.w500, + style: theme.textTheme.labelSmall?.copyWith( + color: + isDark + ? TColors.light.withOpacity(0.8) + : TColors.primary.withOpacity(0.8), + fontWeight: FontWeight.bold, ), ), ), @@ -488,7 +224,7 @@ class SignupWithRoleScreen extends StatelessWidget { Widget _buildSocialLoginButtons( SignupWithRoleController controller, - Color themeColor, + bool isDark, ) { return Column( diff --git a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart index 40bde0b..5e51f53 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/pages/signup/step/officer-information/officer_info_step.dart @@ -29,6 +29,44 @@ class OfficerInfoStep extends StatelessWidget { 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 Obx( () => CustomTextField( @@ -53,7 +91,7 @@ class OfficerInfoStep extends StatelessWidget { controller: controller.rankController, validator: TValidators.validateRank, errorText: controller.rankError.value, - textInputAction: TextInputAction.done, + textInputAction: TextInputAction.next, hintText: 'e.g., Captain', onChanged: (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 = ''; + }, + ), + ), ], ), ); diff --git a/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart b/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart index daeee38..f7a99bb 100644 --- a/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart +++ b/sigap-mobile/lib/src/features/auth/presentasion/widgets/auth_button.dart @@ -21,7 +21,7 @@ class AuthButton extends StatelessWidget { Widget build(BuildContext context) { return SizedBox( width: double.infinity, - height: 50, + height: 55, child: ElevatedButton( onPressed: isLoading ? null : onPressed, style: ElevatedButton.styleFrom( diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart index 291ccb6..06d9100 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/controllers/role_selection_controller.dart @@ -1,9 +1,10 @@ import 'package:get/get.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/utils/constants/app_routes.dart'; import 'package:sigap/src/utils/popups/loaders.dart'; +enum RoleType { viewer, officer } + class RoleSelectionController extends GetxController { static RoleSelectionController get instance => Get.find(); @@ -16,6 +17,9 @@ class RoleSelectionController extends GetxController { // Selected role final Rx selectedRole = Rx(null); + // Role type (Viewer or Officer) + final Rx roleType = RoleType.viewer.obs; + // Loading state final RxBool isLoading = false.obs; final RxBool isOfficer = false.obs; @@ -28,7 +32,6 @@ class RoleSelectionController extends GetxController { super.onInit(); fetchRoles(); } - // Fetch available roles from repository Future fetchRoles() async { @@ -62,44 +65,53 @@ class RoleSelectionController extends GetxController { // Select a role void selectRole(RoleModel 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 - Future continueWithRole() async { - if (selectedRole.value == null) { - TLoaders.errorSnackBar( - title: 'Error', - message: 'Please select a role to continue.', - ); - return; - } + // Future continueWithRole() async { + // if (selectedRole.value == null) { + // TLoaders.errorSnackBar( + // title: 'Error', + // message: 'Please select a role to continue.', + // ); + // return; + // } - try { - isLoading.value = true; + // try { + // isLoading.value = true; - // Logger().i('Selected role: ${selectedRole.value?.name}'); + // // Logger().i('Selected role: ${selectedRole.value?.name}'); - // Check if the selected role is officer - if (selectedRole.value?.name.toLowerCase() == 'officer') { - isOfficer.value = true; - } else { - isOfficer.value = false; - } + // // Check if the selected role is officer + // if (selectedRole.value?.name.toLowerCase() == 'officer') { + // isOfficer.value = true; + // } else { + // isOfficer.value = false; + // } - // Navigate directly to step form with selected role - Get.toNamed( - AppRoutes.registrationForm, - arguments: {'role': selectedRole.value}, - ); - } catch (e) { - TLoaders.errorSnackBar( - title: 'Error', - message: - 'An error occurred while selecting the role. Please try again.', - ); - // Logger().e('Error in continueWithRole: $e'); - } finally { - isLoading.value = false; - } - } + // // Navigate directly to step form with selected role + // Get.toNamed( + // AppRoutes.registrationForm, + // arguments: {'role': selectedRole.value}, + // ); + // } catch (e) { + // TLoaders.errorSnackBar( + // title: 'Error', + // message: + // 'An error occurred while selecting the role. Please try again.', + // ); + // // Logger().e('Error in continueWithRole: $e'); + // } finally { + // isLoading.value = false; + // } + // } } diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart index 374df10..615f2a0 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_selection_screen.dart @@ -10,7 +10,8 @@ import 'package:sigap/src/utils/helpers/helper_functions.dart'; import 'package:sigap/src/utils/loaders/shimmer.dart'; class RoleSelectionScreen extends StatelessWidget { - const RoleSelectionScreen({super.key}); + final void Function(String roleId)? onRoleSelected; + const RoleSelectionScreen({super.key, this.onRoleSelected}); @override Widget build(BuildContext context) { @@ -103,61 +104,10 @@ class RoleSelectionScreen extends StatelessWidget { }), const SizedBox(height: 48), Obx( - () => SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton( - 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, - ), - ), - ), - ), - ), + () => + controller.selectedRole.value != null + ? _SwipeRightHint(isDark: isDark) + : const SizedBox(height: 48), ), ], ), @@ -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 _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(reverse: true); + _animation = Tween( + 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, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart new file mode 100644 index 0000000..5cf34ae --- /dev/null +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/role-selection/role_signup_pageview.dart @@ -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 createState() => _RoleSignupPageViewState(); +} + +class _RoleSignupPageViewState extends State { + 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, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart index 9c5e26e..5f4e086 100644 --- a/sigap-mobile/lib/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart +++ b/sigap-mobile/lib/src/features/onboarding/presentasion/pages/welcome/welcome_screen.dart @@ -87,7 +87,7 @@ class WelcomeScreen extends StatelessWidget { child: ElevatedButton( onPressed: controller.getStarted, style: theme.elevatedButtonTheme.style, - child: Text('Get Started'), + child: Text('Perform Location Check'), ), ), SizedBox(height: isSmallScreen ? TSizes.lg : TSizes.xl), diff --git a/sigap-mobile/lib/src/utils/theme/theme.dart b/sigap-mobile/lib/src/utils/theme/theme.dart index 8aee85b..6b32f85 100644 --- a/sigap-mobile/lib/src/utils/theme/theme.dart +++ b/sigap-mobile/lib/src/utils/theme/theme.dart @@ -35,7 +35,7 @@ class TAppTheme { fontFamily: 'Poppins', disabledColor: TColors.grey, brightness: Brightness.dark, - primaryColor: TColors.primary, + primaryColor: TColors.light, textTheme: TTextTheme.darkTextTheme, chipTheme: TChipTheme.darkChipTheme, scaffoldBackgroundColor: TColors.black, diff --git a/sigap-mobile/lib/src/utils/theme/widget_themes/text_theme.dart b/sigap-mobile/lib/src/utils/theme/widget_themes/text_theme.dart index b7b8aeb..e5d42fd 100644 --- a/sigap-mobile/lib/src/utils/theme/widget_themes/text_theme.dart +++ b/sigap-mobile/lib/src/utils/theme/widget_themes/text_theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import '../../constants/colors.dart'; /// Custom Class for Light & Dark Text Themes