Add animation for empty address and implement custom SliverPersistentHeaderDelegate
- Added a new JSON animation file for empty address in the assets/images/animations directory. - Created a custom SliverPersistentHeaderDelegate class for tab bar functionality in custom_silverbar.dart. - Introduced a global form key management class to streamline form key usage across the application in form_key.dart.
This commit is contained in:
parent
ac39366371
commit
7f6f0c40b7
File diff suppressed because it is too large
Load Diff
|
@ -13,22 +13,12 @@ class AnimatedSplashScreenWidget extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isDark = THelperFunctions.isDarkMode(context);
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
// Try to find SplashController, but don't fail if it's not ready yet
|
|
||||||
// SplashController? splashController;
|
|
||||||
// if (Get.isRegistered<SplashController>()) {
|
|
||||||
// splashController = Get.find<SplashController>();
|
|
||||||
// } else {
|
|
||||||
// // Register a temporary controller if the real one isn't ready
|
|
||||||
// splashController = Get.put(SplashController());
|
|
||||||
// }
|
|
||||||
|
|
||||||
return AnimatedSplashScreen(
|
return AnimatedSplashScreen(
|
||||||
splash: Center(
|
splash: Center(
|
||||||
child: Lottie.asset(
|
child: Lottie.asset(
|
||||||
isDark ? TImages.darkSplashApp : TImages.lightSplashApp,
|
isDark ? TImages.darkSplashApp : TImages.lightSplashApp,
|
||||||
frameRate: FrameRate.max,
|
frameRate: FrameRate.max,
|
||||||
repeat: true,
|
repeat: true,
|
||||||
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
splashIconSize: 300,
|
splashIconSize: 300,
|
||||||
|
@ -38,30 +28,3 @@ class AnimatedSplashScreenWidget extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A transition screen that shows a loading indicator
|
|
||||||
// until authentication is ready
|
|
||||||
// class _LoadingScreen extends StatelessWidget {
|
|
||||||
// const _LoadingScreen();
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// final isDark = THelperFunctions.isDarkMode(context);
|
|
||||||
|
|
||||||
// // This will be shown after the animated splash screen
|
|
||||||
// // while we wait for initialization to complete
|
|
||||||
// return Scaffold(
|
|
||||||
// backgroundColor: isDark ? TColors.dark : TColors.white,
|
|
||||||
// body: const Center(
|
|
||||||
// child: Column(
|
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
// children: [
|
|
||||||
// CircularProgressIndicator(),
|
|
||||||
// SizedBox(height: 24),
|
|
||||||
// Text("Menyiapkan aplikasi..."),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/features/auth/presentasion/pages/email-verification/email_verification_screen.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/forgot-password/forgot_password.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/forgot-password/forgot_password.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/registration-form/registraion_form_screen.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
|
||||||
|
@ -39,6 +40,11 @@ class AppPages {
|
||||||
page: () => const SignupWithRoleScreen(),
|
page: () => const SignupWithRoleScreen(),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
GetPage(
|
||||||
|
name: AppRoutes.emailVerification,
|
||||||
|
page: () => const EmailVerificationScreen(),
|
||||||
|
),
|
||||||
|
|
||||||
GetPage(
|
GetPage(
|
||||||
name: AppRoutes.forgotPassword,
|
name: AppRoutes.forgotPassword,
|
||||||
page: () => const ForgotPasswordScreen(),
|
page: () => const ForgotPasswordScreen(),
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:geocoding/geocoding.dart';
|
import 'package:geocoding/geocoding.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||||
|
|
||||||
class LocationService extends GetxService {
|
class LocationService extends GetxService {
|
||||||
|
@ -143,6 +144,7 @@ class LocationService extends GetxService {
|
||||||
|
|
||||||
// Get city name from coordinates
|
// Get city name from coordinates
|
||||||
if (currentPosition.value != null) {
|
if (currentPosition.value != null) {
|
||||||
|
|
||||||
await _updateCityName();
|
await _updateCityName();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -203,6 +205,8 @@ class LocationService extends GetxService {
|
||||||
if (placemarks.isNotEmpty) {
|
if (placemarks.isNotEmpty) {
|
||||||
currentCity.value = placemarks.first.locality ?? '';
|
currentCity.value = placemarks.first.locality ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Logger().i('Current city: ${currentCity.value}');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
currentCity.value = '';
|
currentCity.value = '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ class SupabaseService extends GetxService {
|
||||||
bool get isAuthenticated => currentUser != null;
|
bool get isAuthenticated => currentUser != null;
|
||||||
|
|
||||||
/// Check if current user is an officer based on metadata
|
/// Check if current user is an officer based on metadata
|
||||||
bool get isOfficer => userMetadata.isOfficer ?? false;
|
bool get isOfficer => userMetadata.isOfficer;
|
||||||
|
|
||||||
/// Get the stored identifier (NIK or NRP) of the current user
|
/// Get the stored identifier (NIK or NRP) of the current user
|
||||||
String? get userIdentifier {
|
String? get userIdentifier {
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
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:sigap/src/cores/services/biometric_service.dart';
|
import 'package:sigap/src/cores/services/biometric_service.dart';
|
||||||
import 'package:sigap/src/cores/services/location_service.dart';
|
import 'package:sigap/src/cores/services/location_service.dart';
|
||||||
import 'package:sigap/src/cores/services/supabase_service.dart';
|
import 'package:sigap/src/cores/services/supabase_service.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
|
import 'package:sigap/src/features/auth/presentasion/pages/signin/signin_screen.dart';
|
||||||
import 'package:sigap/src/features/onboarding/presentasion/pages/onboarding/onboarding_screen.dart';
|
|
||||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/exceptions.dart';
|
||||||
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
import 'package:sigap/src/utils/exceptions/format_exceptions.dart';
|
||||||
|
@ -28,13 +29,17 @@ class AuthenticationRepository extends GetxController {
|
||||||
// Getters that use the Supabase service
|
// Getters that use the Supabase service
|
||||||
User? get authUser => SupabaseService.instance.currentUser;
|
User? get authUser => SupabaseService.instance.currentUser;
|
||||||
String? get currentUserId => SupabaseService.instance.currentUserId;
|
String? get currentUserId => SupabaseService.instance.currentUserId;
|
||||||
|
Session? get currentSession => _supabase.auth.currentSession;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// LIFECYCLE & REDIRECT
|
// LIFECYCLE & REDIRECT
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
screenRedirect();
|
// Delay the redirect to avoid issues during build
|
||||||
|
Future.delayed(Duration.zero, () {
|
||||||
|
screenRedirect();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for biometric login on app start
|
// Check for biometric login on app start
|
||||||
|
@ -63,40 +68,54 @@ class AuthenticationRepository extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect user to appropriate screen on app start
|
/// Updated screenRedirect method to accept arguments
|
||||||
screenRedirect() async {
|
void screenRedirect({UserMetadataModel? arguments}) async {
|
||||||
final session = _supabase.auth.currentSession;
|
// Use addPostFrameCallback to ensure navigation happens after the build cycle
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
try {
|
||||||
|
final session = _supabase.auth.currentSession;
|
||||||
|
|
||||||
// Check if onboarding has been shown before
|
if (await _locationService.isLocationValidForFeature() == false) {
|
||||||
final isFirstTime = storage.read('isFirstTime') ?? false;
|
// Location is not valid, navigate to warning screen
|
||||||
|
Get.offAllNamed(AppRoutes.locationWarning);
|
||||||
if (session != null) {
|
return;
|
||||||
if (session.user.emailConfirmedAt == null) {
|
|
||||||
// User is not verified, go to email verification screen
|
|
||||||
Get.offAllNamed(AppRoutes.emailVerification);
|
|
||||||
} else if (session.user.userMetadata!['profile_status'] == 'incomplete') {
|
|
||||||
// User is regular user, go to main app screen
|
|
||||||
Get.offAllNamed(AppRoutes.registrationForm);
|
|
||||||
} else if (session.user.userMetadata!['profile_status'] == 'complete' &&
|
|
||||||
session.user.emailConfirmedAt != null) {
|
|
||||||
// Redirect to the main app screen
|
|
||||||
Get.offAllNamed(AppRoutes.panicButton);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Try biometric login first
|
|
||||||
bool biometricSuccess = await attemptBiometricLogin();
|
|
||||||
if (!biometricSuccess) {
|
|
||||||
// If not first time, go to sign in directly
|
|
||||||
// If first time, show onboarding first
|
|
||||||
if (isFirstTime) {
|
|
||||||
Get.offAll(() => const SignInScreen());
|
|
||||||
} else {
|
|
||||||
// Mark that onboarding has been shown
|
|
||||||
storage.write('isFirstTime', true);
|
|
||||||
Get.offAll(() => const OnboardingScreen());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
if (session.user.emailConfirmedAt == null) {
|
||||||
|
// User is not verified, go to email verification screen
|
||||||
|
Get.offAllNamed(AppRoutes.emailVerification);
|
||||||
|
} else if (session.user.userMetadata?['profile_status'] ==
|
||||||
|
'incomplete' &&
|
||||||
|
session.user.emailConfirmedAt != null) {
|
||||||
|
// User is incomplete, go to registration form with arguments if provided
|
||||||
|
Get.offAllNamed(AppRoutes.registrationForm);
|
||||||
|
} else {
|
||||||
|
// User is logged in and verified, go to the main app screen
|
||||||
|
Get.offAllNamed(AppRoutes.panicButton);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try biometric login first - but only if we're not already in a navigation
|
||||||
|
if (Get.currentRoute != AppRoutes.signIn &&
|
||||||
|
Get.currentRoute != AppRoutes.onboarding) {
|
||||||
|
bool biometricSuccess = await attemptBiometricLogin();
|
||||||
|
if (!biometricSuccess) {
|
||||||
|
// If not first time, go to sign in directly
|
||||||
|
// If first time, show onboarding first
|
||||||
|
storage.writeIfNull('isFirstTime', true);
|
||||||
|
// check if user is already logged in
|
||||||
|
storage.read('isFirstTime') != true
|
||||||
|
? Get.offAllNamed(AppRoutes.signIn)
|
||||||
|
: Get.offAllNamed(AppRoutes.onboarding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error in screenRedirect: $e');
|
||||||
|
// Fallback to sign in screen on error
|
||||||
|
Get.offAll(() => const SignInScreen());
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
@ -570,9 +589,7 @@ class AuthenticationRepository extends GetxController {
|
||||||
|
|
||||||
// First update auth metadata
|
// First update auth metadata
|
||||||
await _supabase.auth.updateUser(
|
await _supabase.auth.updateUser(
|
||||||
UserAttributes(
|
UserAttributes(data: userMetadataModel.toProfileCompletionJson()),
|
||||||
data: userMetadataModel.toProfileCompletionJson(),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Then update or insert relevant tables based on the role
|
// Then update or insert relevant tables based on the role
|
||||||
|
@ -616,7 +633,6 @@ class AuthenticationRepository extends GetxController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Update user role (officer/user) and metadata
|
// Update user role (officer/user) and metadata
|
||||||
Future<UserResponse> updateUserRole({
|
Future<UserResponse> updateUserRole({
|
||||||
required bool isOfficer,
|
required bool isOfficer,
|
||||||
|
@ -651,7 +667,6 @@ class AuthenticationRepository extends GetxController {
|
||||||
|
|
||||||
// Add these methods to the AuthenticationRepository class
|
// Add these methods to the AuthenticationRepository class
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// BIOMETRIC AUTHENTICATION
|
// BIOMETRIC AUTHENTICATION
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
@ -7,6 +7,9 @@ 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';
|
||||||
|
|
||||||
class EmailVerificationController extends GetxController {
|
class EmailVerificationController extends GetxController {
|
||||||
|
// Singleton instance
|
||||||
|
static EmailVerificationController get instance => Get.find();
|
||||||
|
|
||||||
// OTP text controllers
|
// OTP text controllers
|
||||||
final List<TextEditingController> otpControllers = List.generate(
|
final List<TextEditingController> otpControllers = List.generate(
|
||||||
6,
|
6,
|
||||||
|
|
|
@ -5,6 +5,9 @@ import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
class ForgotPasswordController extends GetxController {
|
class ForgotPasswordController extends GetxController {
|
||||||
|
// Singleton instance
|
||||||
|
static ForgotPasswordController get instance => Get.find();
|
||||||
|
|
||||||
// Form key for validation
|
// Form key for validation
|
||||||
final formKey = GlobalKey<FormState>();
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:get_storage/get_storage.dart';
|
||||||
|
import 'package:logger/logger.dart';
|
||||||
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/auth/data/models/user_metadata_model.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/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/id_card_verification_controller.dart';
|
||||||
|
@ -10,6 +12,7 @@ import 'package:sigap/src/features/auth/presentasion/controllers/steps/unit_info
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/index.dart';
|
import 'package:sigap/src/features/daily-ops/data/models/index.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/utils/constants/app_routes.dart';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/num_int.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
||||||
class FormRegistrationController extends GetxController {
|
class FormRegistrationController extends GetxController {
|
||||||
|
@ -26,6 +29,8 @@ class FormRegistrationController extends GetxController {
|
||||||
late final OfficerInfoController? officerInfoController;
|
late final OfficerInfoController? officerInfoController;
|
||||||
late final UnitInfoController? unitInfoController;
|
late final UnitInfoController? unitInfoController;
|
||||||
|
|
||||||
|
final storage = GetStorage();
|
||||||
|
|
||||||
// Current step index
|
// Current step index
|
||||||
final RxInt currentStep = 0.obs;
|
final RxInt currentStep = 0.obs;
|
||||||
|
|
||||||
|
@ -35,7 +40,7 @@ class FormRegistrationController extends GetxController {
|
||||||
// User metadata model
|
// User metadata model
|
||||||
final Rx<UserMetadataModel> userMetadata = UserMetadataModel().obs;
|
final Rx<UserMetadataModel> userMetadata = UserMetadataModel().obs;
|
||||||
|
|
||||||
// Vievewer data
|
// Viewer data
|
||||||
final Rx<UserModel?> viewerModel = Rx<UserModel?>(null);
|
final Rx<UserModel?> viewerModel = Rx<UserModel?>(null);
|
||||||
final Rx<ProfileModel?> profileModel = Rx<ProfileModel?>(null);
|
final Rx<ProfileModel?> profileModel = Rx<ProfileModel?>(null);
|
||||||
|
|
||||||
|
@ -48,77 +53,207 @@ class FormRegistrationController extends GetxController {
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
// Initialize user data directly from current session
|
||||||
|
_initializeFromCurrentUser();
|
||||||
|
}
|
||||||
|
|
||||||
// Get role and initial data from arguments
|
/// Initialize the controller directly from current user session
|
||||||
final arguments = Get.arguments;
|
void _initializeFromCurrentUser() async {
|
||||||
if (arguments != null) {
|
try {
|
||||||
if (arguments['role'] != null) {
|
Logger().d('Initializing registration form from current user');
|
||||||
selectedRole.value = arguments['role'] as RoleModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (arguments['initialData'] != null) {
|
// Get the current user session from AuthenticationRepository
|
||||||
// Initialize with data from signup
|
final session = AuthenticationRepository.instance.currentSession;
|
||||||
userMetadata.value = arguments['initialData'] as UserMetadataModel;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store userId if provided
|
// Initialize with default metadata
|
||||||
if (arguments['userId'] != null) {
|
UserMetadataModel metadata = const UserMetadataModel(
|
||||||
userMetadata.value = userMetadata.value.copyWith(
|
profileStatus: 'incomplete',
|
||||||
userId: arguments['userId'],
|
isOfficer: false,
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize userMetadata with the selected role information
|
|
||||||
if ((userMetadata.value.roleId?.isEmpty ?? true) &&
|
|
||||||
selectedRole.value != null) {
|
|
||||||
userMetadata.value = userMetadata.value.copyWith(
|
|
||||||
roleId: selectedRole.value!.id,
|
|
||||||
isOfficer: selectedRole.value!.isOfficer,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_initializeControllers();
|
|
||||||
} else {
|
|
||||||
TLoaders.errorSnackBar(
|
|
||||||
title: 'Error',
|
|
||||||
message: 'No role selected. Please go back and select a role.',
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedRole.value?.isOfficer == true) {
|
// If there is an active session, use that data
|
||||||
_fetchAvailableUnits();
|
if (session?.user != null) {
|
||||||
|
final user = session!.user;
|
||||||
|
Logger().d('Found active user session: ${user.id} - ${user.email}');
|
||||||
|
|
||||||
|
// Extract metadata from user session
|
||||||
|
metadata = UserMetadataModel(
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
roleId: user.userMetadata?['role_id'] as String?,
|
||||||
|
isOfficer: user.userMetadata?['is_officer'] as bool? ?? false,
|
||||||
|
profileStatus:
|
||||||
|
user.userMetadata?['profile_status'] as String? ?? 'incomplete',
|
||||||
|
);
|
||||||
|
|
||||||
|
// If user has additional metadata and it's in the expected format, use it
|
||||||
|
if (user.userMetadata != null) {
|
||||||
|
try {
|
||||||
|
// Try to parse complete metadata if available
|
||||||
|
final fullMetadata = UserMetadataModel.fromJson(user.userMetadata);
|
||||||
|
metadata = fullMetadata;
|
||||||
|
Logger().d('Successfully parsed complete user metadata');
|
||||||
|
} catch (e) {
|
||||||
|
Logger().w('Could not parse full metadata object: $e');
|
||||||
|
// Continue with the basic metadata already created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No active session, check if any arguments were passed
|
||||||
|
final arguments = Get.arguments;
|
||||||
|
|
||||||
|
// If arguments contain a user ID, use it as fallback
|
||||||
|
if (arguments is Map<String, dynamic> &&
|
||||||
|
arguments.containsKey('userId')) {
|
||||||
|
metadata = metadata.copyWith(
|
||||||
|
userId: arguments['userId'] as String?,
|
||||||
|
email: arguments['email'] as String?,
|
||||||
|
roleId: arguments['roleId'] as String?,
|
||||||
|
isOfficer: arguments['isOfficer'] as bool? ?? false,
|
||||||
|
);
|
||||||
|
Logger().d('Using arguments as fallback: ${metadata.userId}');
|
||||||
|
} else {
|
||||||
|
// No user data available, check temporary storage
|
||||||
|
final tempUserId = storage.read('TEMP_USER_ID') as String?;
|
||||||
|
final tempEmail = storage.read('CURRENT_USER_EMAIL') as String?;
|
||||||
|
|
||||||
|
if (tempUserId != null || tempEmail != null) {
|
||||||
|
metadata = metadata.copyWith(
|
||||||
|
userId: tempUserId,
|
||||||
|
email: tempEmail,
|
||||||
|
roleId: storage.read('TEMP_ROLE_ID') as String?,
|
||||||
|
isOfficer: storage.read('TEMP_IS_OFFICER') as bool? ?? false,
|
||||||
|
);
|
||||||
|
Logger().d(
|
||||||
|
'Using temporary storage as fallback: ${metadata.userId}',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Logger().w('No user data available, using default empty metadata');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the user metadata
|
||||||
|
userMetadata.value = metadata;
|
||||||
|
Logger().d('Final user metadata: ${userMetadata.value.toString()}');
|
||||||
|
|
||||||
|
// Complete initialization
|
||||||
|
await _finalizeInitialization();
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error initializing from current user: $e');
|
||||||
|
userMetadata.value = const UserMetadataModel(
|
||||||
|
profileStatus: 'incomplete',
|
||||||
|
isOfficer: false,
|
||||||
|
);
|
||||||
|
await _finalizeInitialization();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finalize initialization after metadata is set
|
||||||
|
Future<void> _finalizeInitialization() async {
|
||||||
|
try {
|
||||||
|
// Initialize form controllers
|
||||||
|
_initializeControllers();
|
||||||
|
|
||||||
|
// Set role information if available
|
||||||
|
if (userMetadata.value.roleId?.isNotEmpty == true) {
|
||||||
|
await _setRoleFromMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch units if user is an officer
|
||||||
|
if (userMetadata.value.isOfficer ||
|
||||||
|
(selectedRole.value?.isOfficer == true)) {
|
||||||
|
await _fetchAvailableUnits();
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger().d('Initialization completed successfully');
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error in finalization: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set role information from metadata
|
||||||
|
Future<void> _setRoleFromMetadata() async {
|
||||||
|
try {
|
||||||
|
final roleId = userMetadata.value.roleId;
|
||||||
|
if (roleId?.isNotEmpty == true) {
|
||||||
|
// Try to find the role in available roles
|
||||||
|
final role = await _findRoleById(roleId!);
|
||||||
|
if (role != null) {
|
||||||
|
selectedRole.value = role;
|
||||||
|
Logger().d('Role set from metadata: ${role.name}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error setting role from metadata: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find role by ID (implement based on your role management system)
|
||||||
|
Future<RoleModel?> _findRoleById(String roleId) async {
|
||||||
|
try {
|
||||||
|
// Implement based on your role fetching logic
|
||||||
|
// This is a placeholder - replace with your actual implementation
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
Logger().e('Error finding role by ID: $e');
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initializeControllers() {
|
void _initializeControllers() {
|
||||||
final isOfficer = selectedRole.value?.isOfficer ?? false;
|
final isOfficer = userMetadata.value.isOfficer;
|
||||||
|
|
||||||
// Always initialize personal info controller
|
// Initialize controllers with built-in static form keys
|
||||||
personalInfoController = Get.put(PersonalInfoController());
|
Get.put<PersonalInfoController>(PersonalInfoController(), permanent: false);
|
||||||
|
|
||||||
// Initialize ID Card verification controller
|
Get.put<IdCardVerificationController>(
|
||||||
idCardVerificationController = Get.put(
|
|
||||||
IdCardVerificationController(isOfficer: isOfficer),
|
IdCardVerificationController(isOfficer: isOfficer),
|
||||||
|
permanent: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize Selfie verification controller
|
Get.put<SelfieVerificationController>(
|
||||||
selfieVerificationController = Get.put(SelfieVerificationController());
|
SelfieVerificationController(),
|
||||||
|
permanent: false,
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize identity verification controller
|
Get.put<IdentityVerificationController>(
|
||||||
identityController = Get.put(
|
|
||||||
IdentityVerificationController(isOfficer: isOfficer),
|
IdentityVerificationController(isOfficer: isOfficer),
|
||||||
|
permanent: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Initialize officer-specific controllers only if user is an officer
|
||||||
if (isOfficer) {
|
if (isOfficer) {
|
||||||
// Initialize officer-specific controllers
|
Get.put<OfficerInfoController>(OfficerInfoController(), permanent: false);
|
||||||
officerInfoController = Get.put(OfficerInfoController());
|
|
||||||
unitInfoController = Get.put(UnitInfoController());
|
Get.put<UnitInfoController>(UnitInfoController(), permanent: false);
|
||||||
totalSteps = 5; // Personal, ID Card, Selfie, Officer Info, Unit Info
|
|
||||||
|
totalSteps =
|
||||||
|
TNum.totalStepOfficer; // Personal, ID Card, Selfie, Officer Info, Unit Info
|
||||||
|
|
||||||
|
// Assign officer-specific controllers
|
||||||
|
officerInfoController = Get.find<OfficerInfoController>();
|
||||||
|
unitInfoController = Get.find<UnitInfoController>();
|
||||||
} else {
|
} else {
|
||||||
// For civilian users
|
// For civilian users
|
||||||
officerInfoController = null;
|
officerInfoController = null;
|
||||||
unitInfoController = null;
|
unitInfoController = null;
|
||||||
totalSteps = 4; // Personal, ID Card, Selfie, Identity
|
totalSteps = TNum.totalStepViewer; // Personal, ID Card, Selfie, Identity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assign shared controllers
|
||||||
|
personalInfoController = Get.find<PersonalInfoController>();
|
||||||
|
idCardVerificationController = Get.find<IdCardVerificationController>();
|
||||||
|
selfieVerificationController = Get.find<SelfieVerificationController>();
|
||||||
|
identityController = Get.find<IdentityVerificationController>();
|
||||||
|
|
||||||
|
// Initialize selectedRole based on isOfficer
|
||||||
|
if (selectedRole.value == null &&
|
||||||
|
userMetadata.value.additionalData != null) {
|
||||||
|
final roleData = userMetadata.value.additionalData?['role'];
|
||||||
|
if (roleData != null) {
|
||||||
|
selectedRole.value = roleData as RoleModel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,7 +450,6 @@ class FormRegistrationController extends GetxController {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Regular user - create profile-related data
|
// Regular user - create profile-related data
|
||||||
final viewerData = viewerModel.value?.copyWith(
|
final viewerData = viewerModel.value?.copyWith(
|
||||||
phone: personalInfoController.phoneController.text,
|
phone: personalInfoController.phoneController.text,
|
||||||
|
|
|
@ -7,6 +7,9 @@ import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
||||||
class SignInController extends GetxController {
|
class SignInController extends GetxController {
|
||||||
|
// Singleton instance
|
||||||
|
static SignInController get instance => Get.find();
|
||||||
|
|
||||||
final rememberMe = false.obs;
|
final rememberMe = false.obs;
|
||||||
final isPasswordVisible = false.obs;
|
final isPasswordVisible = false.obs;
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
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/models/user_metadata_model.dart';
|
import 'package:sigap/src/features/auth/data/models/user_metadata_model.dart';
|
||||||
|
import 'package:sigap/src/features/auth/data/repositories/authentication_repository.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
|
|
|
@ -12,6 +12,7 @@ import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
import 'package:sigap/src/utils/helpers/network_manager.dart';
|
||||||
import 'package:sigap/src/utils/popups/loaders.dart';
|
import 'package:sigap/src/utils/popups/loaders.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
// Define the role types
|
// Define the role types
|
||||||
enum RoleType { viewer, officer }
|
enum RoleType { viewer, officer }
|
||||||
|
@ -166,6 +167,7 @@ class SignupWithRoleController extends GetxController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sign up function
|
// Sign up function
|
||||||
|
/// Updated signup function with better error handling and argument passing
|
||||||
void signUp(bool isOfficer) async {
|
void signUp(bool isOfficer) async {
|
||||||
if (!validateSignupForm()) {
|
if (!validateSignupForm()) {
|
||||||
return;
|
return;
|
||||||
|
@ -184,21 +186,30 @@ class SignupWithRoleController extends GetxController {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we have a role selected
|
// Ensure we have a role selected
|
||||||
if (selectedRoleId.value.isEmpty) {
|
if (selectedRoleId.value.isEmpty) {
|
||||||
// Find a role based on the selected role type
|
|
||||||
_updateSelectedRoleBasedOnType();
|
_updateSelectedRoleBasedOnType();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create initial user metadata
|
// Validate role selection
|
||||||
|
if (selectedRoleId.value.isEmpty) {
|
||||||
|
TLoaders.errorSnackBar(
|
||||||
|
title: 'Role Required',
|
||||||
|
message: 'Please select a role before continuing.',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create comprehensive initial user metadata
|
||||||
final initialMetadata = UserMetadataModel(
|
final initialMetadata = UserMetadataModel(
|
||||||
email: emailController.text.trim(),
|
email: emailController.text.trim(),
|
||||||
roleId: selectedRoleId.value,
|
roleId: selectedRoleId.value,
|
||||||
isOfficer: isOfficer,
|
isOfficer: isOfficer,
|
||||||
|
profileStatus: 'incomplete',
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First create the basic account with email/password
|
// Create the account
|
||||||
final authResponse = await AuthenticationRepository.instance
|
final authResponse = await AuthenticationRepository.instance
|
||||||
.initialSignUp(
|
.initialSignUp(
|
||||||
email: emailController.text.trim(),
|
email: emailController.text.trim(),
|
||||||
|
@ -206,48 +217,57 @@ class SignupWithRoleController extends GetxController {
|
||||||
initialData: initialMetadata,
|
initialData: initialMetadata,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if authResponse has a user property
|
// Validate response
|
||||||
if (authResponse.user == null || authResponse.session == null) {
|
if (authResponse.session == null || authResponse.user == null) {
|
||||||
throw Exception('Failed to create account. Please try again.');
|
throw Exception('Failed to create account. Please try again.');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store email for verification
|
final user = authResponse.user!;
|
||||||
storage.write('CURRENT_USER_EMAIL', emailController.text.trim());
|
Logger().d('Account created successfully for user: ${user.id}');
|
||||||
storage.write('TEMP_AUTH_TOKEN', authResponse.session?.accessToken);
|
|
||||||
storage.write('TEMP_USER_ID', authResponse.user?.id);
|
// Store temporary data for verification process
|
||||||
storage.write('TEMP_ROLE_ID', selectedRoleId.value);
|
await _storeTemporaryData(authResponse, isOfficer);
|
||||||
|
|
||||||
|
// Navigate with arguments
|
||||||
|
AuthenticationRepository.instance.screenRedirect();
|
||||||
|
|
||||||
// Navigate to registration form
|
|
||||||
Get.offNamed(
|
|
||||||
AppRoutes.registrationForm,
|
|
||||||
arguments: {
|
|
||||||
'role': selectedRole.value,
|
|
||||||
'userId': authResponse.user?.id,
|
|
||||||
'initialData': initialMetadata,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch (authError) {
|
} catch (authError) {
|
||||||
// Handle specific authentication errors
|
Logger().e('Authentication error during signup: $authError');
|
||||||
Logger().e('Error during signup: $authError');
|
|
||||||
TLoaders.errorSnackBar(
|
TLoaders.errorSnackBar(
|
||||||
title: 'Registration Failed',
|
title: 'Registration Failed',
|
||||||
message: authError.toString(),
|
message: _getReadableErrorMessage(authError.toString()),
|
||||||
);
|
);
|
||||||
// Important: Do not navigate or redirect on error
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Logger().e('Error during signup: $e');
|
Logger().e('Unexpected error during signup: $e');
|
||||||
TLoaders.errorSnackBar(
|
TLoaders.errorSnackBar(
|
||||||
title: 'Registration Failed',
|
title: 'Registration Failed',
|
||||||
message: e.toString(),
|
message: 'An unexpected error occurred. Please try again.',
|
||||||
);
|
);
|
||||||
// No navigation on error
|
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
@ -517,7 +537,7 @@ class SignupWithRoleController extends GetxController {
|
||||||
|
|
||||||
// Navigate to registration form to complete profile
|
// Navigate to registration form to complete profile
|
||||||
Get.offNamed(
|
Get.offNamed(
|
||||||
AppRoutes.registrationForm,
|
AppRoutes.emailVerification,
|
||||||
arguments: {
|
arguments: {
|
||||||
'role': selectedRole.value,
|
'role': selectedRole.value,
|
||||||
'userId': userId,
|
'userId': userId,
|
||||||
|
@ -545,4 +565,19 @@ class SignupWithRoleController extends GetxController {
|
||||||
void goToSignIn() {
|
void goToSignIn() {
|
||||||
Get.offNamed(AppRoutes.signIn);
|
Get.offNamed(AppRoutes.signIn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert technical error messages to user-friendly messages
|
||||||
|
String _getReadableErrorMessage(String error) {
|
||||||
|
if (error.contains('email')) {
|
||||||
|
return 'Please check your email address and try again.';
|
||||||
|
} else if (error.contains('password')) {
|
||||||
|
return 'Password must be at least 6 characters long.';
|
||||||
|
} else if (error.contains('network') || error.contains('connection')) {
|
||||||
|
return 'Network error. Please check your connection and try again.';
|
||||||
|
} else if (error.contains('already registered') ||
|
||||||
|
error.contains('already exists')) {
|
||||||
|
return 'This email is already registered. Please try signing in instead.';
|
||||||
|
}
|
||||||
|
return 'Registration failed. Please try again.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,20 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/form_key.dart';
|
||||||
|
|
||||||
class IdCardVerificationController extends GetxController {
|
class IdCardVerificationController extends GetxController {
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
// Singleton instance
|
||||||
|
static IdCardVerificationController get instance => Get.find();
|
||||||
|
|
||||||
|
// Static form key
|
||||||
|
final GlobalKey<FormState> formKey = TGlobalFormKey.idCardVerification();
|
||||||
final AzureOCRService _ocrService = AzureOCRService();
|
final AzureOCRService _ocrService = AzureOCRService();
|
||||||
final bool isOfficer;
|
final bool isOfficer;
|
||||||
|
|
||||||
|
// Maximum allowed file size in bytes (4MB)
|
||||||
|
final int maxFileSizeBytes = 4 * 1024 * 1024; // 4MB in bytes
|
||||||
|
|
||||||
IdCardVerificationController({required this.isOfficer});
|
IdCardVerificationController({required this.isOfficer});
|
||||||
|
|
||||||
// ID Card variables
|
// ID Card variables
|
||||||
|
@ -51,7 +59,7 @@ class IdCardVerificationController extends GetxController {
|
||||||
idCardValidationMessage.value = '';
|
idCardValidationMessage.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pick ID Card Image
|
// Pick ID Card Image with file size validation
|
||||||
Future<void> pickIdCardImage(ImageSource source) async {
|
Future<void> pickIdCardImage(ImageSource source) async {
|
||||||
try {
|
try {
|
||||||
isUploadingIdCard.value = true;
|
isUploadingIdCard.value = true;
|
||||||
|
@ -61,10 +69,21 @@ class IdCardVerificationController extends GetxController {
|
||||||
final ImagePicker picker = ImagePicker();
|
final ImagePicker picker = ImagePicker();
|
||||||
final XFile? image = await picker.pickImage(
|
final XFile? image = await picker.pickImage(
|
||||||
source: source,
|
source: source,
|
||||||
imageQuality: 80,
|
imageQuality: 80, // Reduce quality to help with file size
|
||||||
);
|
);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
|
// Check file size
|
||||||
|
final File file = File(image.path);
|
||||||
|
final int fileSize = await file.length();
|
||||||
|
|
||||||
|
if (fileSize > maxFileSizeBytes) {
|
||||||
|
idCardError.value =
|
||||||
|
'Image size exceeds 4MB limit. Please choose a smaller image or lower resolution.';
|
||||||
|
isIdCardValid.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add artificial delay to show loading state
|
// Add artificial delay to show loading state
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
idCardImage.value = image;
|
idCardImage.value = image;
|
||||||
|
|
|
@ -2,10 +2,15 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
||||||
import 'package:sigap/src/features/auth/presentasion/controllers/steps/image_verification_controller.dart';
|
import 'package:sigap/src/features/auth/presentasion/controllers/steps/image_verification_controller.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/form_key.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
class IdentityVerificationController extends GetxController {
|
class IdentityVerificationController extends GetxController {
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
// Singleton instance
|
||||||
|
static IdentityVerificationController get instance => Get.find();
|
||||||
|
|
||||||
|
// Static form key
|
||||||
|
final GlobalKey<FormState> formKey = TGlobalFormKey.identityVerification();
|
||||||
final AzureOCRService _ocrService = AzureOCRService();
|
final AzureOCRService _ocrService = AzureOCRService();
|
||||||
final bool isOfficer;
|
final bool isOfficer;
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,9 @@ import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
||||||
|
|
||||||
class ImageVerificationController extends GetxController {
|
class ImageVerificationController extends GetxController {
|
||||||
|
// Singleton instance
|
||||||
|
static ImageVerificationController get instance => Get.find();
|
||||||
|
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||||
final AzureOCRService _ocrService = AzureOCRService();
|
final AzureOCRService _ocrService = AzureOCRService();
|
||||||
final bool isOfficer;
|
final bool isOfficer;
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/form_key.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
class OfficerInfoController extends GetxController {
|
class OfficerInfoController extends GetxController {
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
// Singleton instance
|
||||||
|
static OfficerInfoController get instance => Get.find();
|
||||||
|
|
||||||
|
// Static form key
|
||||||
|
final GlobalKey<FormState> formKey = TGlobalFormKey.officerInfo();
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
final nrpController = TextEditingController();
|
final nrpController = TextEditingController();
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/form_key.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
class PersonalInfoController extends GetxController {
|
class PersonalInfoController extends GetxController {
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
// Singleton instance
|
||||||
|
static PersonalInfoController get instance => Get.find();
|
||||||
|
|
||||||
|
// Static form key
|
||||||
|
final GlobalKey<FormState> formKey = TGlobalFormKey.personalInfo();
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
final firstNameController = TextEditingController();
|
final firstNameController = TextEditingController();
|
||||||
|
@ -21,6 +26,7 @@ class PersonalInfoController extends GetxController {
|
||||||
final RxString bioError = ''.obs;
|
final RxString bioError = ''.obs;
|
||||||
final RxString addressError = ''.obs;
|
final RxString addressError = ''.obs;
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
|
@ -1,12 +1,22 @@
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
import 'package:sigap/src/cores/services/azure_ocr_service.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/form_key.dart';
|
||||||
|
|
||||||
class SelfieVerificationController extends GetxController {
|
class SelfieVerificationController extends GetxController {
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
// Singleton instance
|
||||||
|
static SelfieVerificationController get instance => Get.find();
|
||||||
|
|
||||||
|
// Static form key
|
||||||
|
final GlobalKey<FormState> formKey = TGlobalFormKey.selfieVerification();
|
||||||
final AzureOCRService _ocrService = AzureOCRService();
|
final AzureOCRService _ocrService = AzureOCRService();
|
||||||
|
|
||||||
|
// Maximum allowed file size in bytes (4MB)
|
||||||
|
final int maxFileSizeBytes = 4 * 1024 * 1024; // 4MB in bytes
|
||||||
|
|
||||||
// Face verification variables
|
// Face verification variables
|
||||||
final Rx<XFile?> selfieImage = Rx<XFile?>(null);
|
final Rx<XFile?> selfieImage = Rx<XFile?>(null);
|
||||||
final RxString selfieError = RxString('');
|
final RxString selfieError = RxString('');
|
||||||
|
@ -51,7 +61,7 @@ class SelfieVerificationController extends GetxController {
|
||||||
selfieValidationMessage.value = '';
|
selfieValidationMessage.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take or pick selfie image
|
// Take or pick selfie image with file size validation
|
||||||
Future<void> pickSelfieImage(ImageSource source) async {
|
Future<void> pickSelfieImage(ImageSource source) async {
|
||||||
try {
|
try {
|
||||||
isUploadingSelfie.value = true;
|
isUploadingSelfie.value = true;
|
||||||
|
@ -62,10 +72,21 @@ class SelfieVerificationController extends GetxController {
|
||||||
final XFile? image = await picker.pickImage(
|
final XFile? image = await picker.pickImage(
|
||||||
source: source,
|
source: source,
|
||||||
preferredCameraDevice: CameraDevice.front,
|
preferredCameraDevice: CameraDevice.front,
|
||||||
imageQuality: 80,
|
imageQuality: 80, // Reduce quality to help with file size
|
||||||
);
|
);
|
||||||
|
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
|
// Check file size
|
||||||
|
final File file = File(image.path);
|
||||||
|
final int fileSize = await file.length();
|
||||||
|
|
||||||
|
if (fileSize > maxFileSizeBytes) {
|
||||||
|
selfieError.value =
|
||||||
|
'Image size exceeds 4MB limit. Please take a lower resolution photo.';
|
||||||
|
isSelfieValid.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Add artificial delay to show loading state
|
// Add artificial delay to show loading state
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
selfieImage.value = image;
|
selfieImage.value = image;
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:sigap/src/features/daily-ops/data/models/index.dart';
|
import 'package:sigap/src/features/daily-ops/data/models/index.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/form_key.dart';
|
||||||
import 'package:sigap/src/utils/validators/validation.dart';
|
import 'package:sigap/src/utils/validators/validation.dart';
|
||||||
|
|
||||||
class UnitInfoController extends GetxController {
|
class UnitInfoController extends GetxController {
|
||||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
// Singleton instance
|
||||||
|
static UnitInfoController get instance => Get.find();
|
||||||
|
|
||||||
|
// Static form key
|
||||||
|
final GlobalKey<FormState> formKey = TGlobalFormKey.unitInfo();
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
final positionController = TextEditingController();
|
final positionController = TextEditingController();
|
||||||
|
|
|
@ -34,15 +34,14 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: dark ? TColors.dark : TColors.light,
|
backgroundColor: dark ? TColors.dark : TColors.light,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: Obx(
|
title: Text(
|
||||||
() => Text(
|
'Complete Your Profile',
|
||||||
'Complete Your ${controller.selectedRole.value?.name ?? ""} Profile',
|
style: Theme.of(
|
||||||
style: Theme.of(
|
context,
|
||||||
context,
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
|
@ -55,9 +54,19 @@ class FormRegistrationScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Obx(() {
|
body: Obx(() {
|
||||||
// Show loading while initializing
|
// Make loading check more robust - showing a loading state while controller initializes
|
||||||
if (controller.selectedRole.value == null) {
|
if (controller.userMetadata.value.userId == null &&
|
||||||
return const Center(child: CircularProgressIndicator());
|
controller.userMetadata.value.roleId == null) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text("Loading profile information..."),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
|
|
|
@ -209,14 +209,14 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
height: 180,
|
height: 180,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: backgroundColor, // Using the dynamic background color
|
color: backgroundColor,
|
||||||
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
borderRadius: BorderRadius.circular(TSizes.borderRadiusMd),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: borderColor,
|
color: borderColor,
|
||||||
width:
|
width:
|
||||||
controller.idCardError.value.isNotEmpty
|
controller.idCardError.value.isNotEmpty
|
||||||
? 2
|
? 2
|
||||||
: 1, // Thicker border for error state
|
: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child:
|
child:
|
||||||
|
@ -262,7 +262,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.xs),
|
const SizedBox(height: TSizes.xs),
|
||||||
Text(
|
Text(
|
||||||
'Tap to select an image',
|
'Tap to select an image (max 4MB)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: TSizes.fontSizeSm,
|
fontSize: TSizes.fontSizeSm,
|
||||||
color:
|
color:
|
||||||
|
@ -294,8 +294,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
TSizes.borderRadiusMd -
|
TSizes.borderRadiusMd - 2,
|
||||||
2, // Adjust for border width
|
|
||||||
),
|
),
|
||||||
child: Image.file(
|
child: Image.file(
|
||||||
File(controller.idCardImage.value!.path),
|
File(controller.idCardImage.value!.path),
|
||||||
|
@ -313,8 +312,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
TSizes.borderRadiusMd -
|
TSizes.borderRadiusMd - 2,
|
||||||
2, // Adjust for border width
|
|
||||||
),
|
),
|
||||||
color: TColors.error.withOpacity(0.2),
|
color: TColors.error.withOpacity(0.2),
|
||||||
),
|
),
|
||||||
|
@ -341,7 +339,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
horizontal: TSizes.md,
|
horizontal: TSizes.md,
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Please upload a clearer image',
|
controller.idCardError.value,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: TColors.error,
|
color: TColors.error,
|
||||||
|
@ -361,8 +359,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
TSizes.borderRadiusMd -
|
TSizes.borderRadiusMd - 2,
|
||||||
2, // Adjust for border width
|
|
||||||
),
|
),
|
||||||
color: Colors.black.withOpacity(0.5),
|
color: Colors.black.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
|
@ -429,6 +426,37 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
minimumSize: const Size(double.infinity, 50),
|
minimumSize: const Size(double.infinity, 50),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Show file size information if image is uploaded
|
||||||
|
if (controller.idCardImage.value != null)
|
||||||
|
FutureBuilder<int>(
|
||||||
|
future: File(controller.idCardImage.value!.path).length(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final fileSizeKB = snapshot.data! / 1024;
|
||||||
|
final fileSizeMB = fileSizeKB / 1024;
|
||||||
|
final isOversized =
|
||||||
|
snapshot.data! > controller.maxFileSizeBytes;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: TSizes.sm),
|
||||||
|
child: Text(
|
||||||
|
'File size: ${fileSizeMB.toStringAsFixed(2)} MB',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TSizes.fontSizeSm,
|
||||||
|
color:
|
||||||
|
isOversized
|
||||||
|
? TColors.error
|
||||||
|
: TColors.textSecondary,
|
||||||
|
fontWeight:
|
||||||
|
isOversized
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -443,7 +471,7 @@ class IdCardVerificationStep extends StatelessWidget {
|
||||||
final String idCardType = isOfficer ? 'KTA' : 'KTP';
|
final String idCardType = isOfficer ? 'KTA' : 'KTP';
|
||||||
final String title = 'Select $idCardType Image Source';
|
final String title = 'Select $idCardType Image Source';
|
||||||
final String message =
|
final String message =
|
||||||
'Please ensure your ID card is clear, well-lit, and all text is readable';
|
'Please ensure your ID card is clear, well-lit, and all text is readable. Maximum file size: 4MB';
|
||||||
|
|
||||||
Get.dialog(
|
Get.dialog(
|
||||||
Dialog(
|
Dialog(
|
||||||
|
|
|
@ -327,7 +327,7 @@ class SelfieVerificationStep extends StatelessWidget {
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.xs),
|
const SizedBox(height: TSizes.xs),
|
||||||
Text(
|
Text(
|
||||||
'Tap to open camera',
|
'Tap to take a selfie (max 4MB)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: TSizes.fontSizeSm,
|
fontSize: TSizes.fontSizeSm,
|
||||||
color:
|
color:
|
||||||
|
@ -517,6 +517,37 @@ class SelfieVerificationStep extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// File size information
|
||||||
|
if (controller.selfieImage.value != null)
|
||||||
|
FutureBuilder<int>(
|
||||||
|
future: File(controller.selfieImage.value!.path).length(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
final fileSizeKB = snapshot.data! / 1024;
|
||||||
|
final fileSizeMB = fileSizeKB / 1024;
|
||||||
|
final isOversized =
|
||||||
|
snapshot.data! > controller.maxFileSizeBytes;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: TSizes.sm),
|
||||||
|
child: Text(
|
||||||
|
'File size: ${fileSizeMB.toStringAsFixed(2)} MB',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: TSizes.fontSizeSm,
|
||||||
|
color:
|
||||||
|
isOversized
|
||||||
|
? TColors.error
|
||||||
|
: TColors.textSecondary,
|
||||||
|
fontWeight:
|
||||||
|
isOversized
|
||||||
|
? FontWeight.bold
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -8,6 +8,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/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';
|
||||||
|
@ -34,134 +35,133 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Obx(
|
body: Obx(
|
||||||
() => Column(
|
() => NestedScrollView(
|
||||||
children: [
|
headerSliverBuilder: (context, innerBoxIsScrolled) {
|
||||||
// Top section with image and role information
|
return [
|
||||||
_buildTopImageSection(controller, context),
|
// Top image section as SliverAppBar
|
||||||
|
_buildSliverAppBar(controller, context),
|
||||||
|
|
||||||
// Bottom section with form
|
// Tab bar as pinned SliverPersistentHeader
|
||||||
Expanded(
|
SliverPersistentHeader(
|
||||||
child: Container(
|
delegate: TSliverTabBarDelegate(
|
||||||
decoration: BoxDecoration(
|
child: _buildTabBar(context, controller),
|
||||||
color: isDark ? TColors.dark : TColors.white,
|
minHeight: 70, // Height including padding
|
||||||
borderRadius: BorderRadius.only(
|
maxHeight: 70, // Fixed height for the tab bar
|
||||||
topLeft: Radius.circular(TSizes.borderRadiusLg),
|
),
|
||||||
topRight: Radius.circular(TSizes.borderRadiusLg),
|
pinned: true,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
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),
|
||||||
|
]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
boxShadow: [
|
// Add extra padding at the bottom for safe area
|
||||||
BoxShadow(
|
SliverToBoxAdapter(
|
||||||
color: Colors.black.withOpacity(0.1),
|
child: SizedBox(
|
||||||
blurRadius: 10,
|
height: MediaQuery.of(context).padding.bottom,
|
||||||
offset: const Offset(0, -5),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Tab bar for switching between viewer and officer
|
|
||||||
_buildTabBar(context, controller),
|
|
||||||
|
|
||||||
// Form content
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(TSizes.defaultSpace),
|
|
||||||
child: _buildSignupForm(context, controller),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTopImageSection(
|
SliverAppBar _buildSliverAppBar(
|
||||||
SignupWithRoleController controller,
|
SignupWithRoleController controller,
|
||||||
BuildContext context,
|
BuildContext context
|
||||||
) {
|
) {
|
||||||
bool isOfficer = controller.roleType.value == RoleType.officer;
|
bool isOfficer = controller.roleType.value == RoleType.officer;
|
||||||
final isDark = THelperFunctions.isDarkMode(context);
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
final topPadding = MediaQuery.of(context).padding.top;
|
||||||
|
|
||||||
return Container(
|
return SliverAppBar(
|
||||||
height:
|
expandedHeight: MediaQuery.of(context).size.height * 0.35,
|
||||||
MediaQuery.of(context).size.height *
|
pinned: true,
|
||||||
0.35, // Take 35% of screen height
|
backgroundColor: isDark ? TColors.dark : TColors.primary,
|
||||||
color: isDark ? TColors.dark : TColors.primary,
|
elevation: 0,
|
||||||
child: Stack(
|
automaticallyImplyLeading: false,
|
||||||
children: [
|
flexibleSpace: FlexibleSpaceBar(
|
||||||
// Background gradient
|
background: Stack(
|
||||||
Positioned.fill(
|
children: [
|
||||||
child: Container(
|
// Background gradient
|
||||||
decoration: BoxDecoration(
|
Positioned.fill(
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
colors: [
|
|
||||||
isDark ? Colors.black : TColors.primary,
|
|
||||||
isDark ? TColors.dark : TColors.primary.withOpacity(0.8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Back button
|
|
||||||
Positioned(
|
|
||||||
top: MediaQuery.of(context).padding.top + TSizes.sm,
|
|
||||||
left: TSizes.sm,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => Get.back(),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(TSizes.sm),
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white.withOpacity(0.2),
|
gradient: LinearGradient(
|
||||||
shape: BoxShape.circle,
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
isDark ? Colors.black : TColors.primary,
|
||||||
|
isDark ? TColors.dark : TColors.primary.withOpacity(0.8),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: const Icon(Icons.arrow_back, color: Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// Role image and text content
|
// Role image
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: LayoutBuilder(
|
child: LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
// Responsive image size based on available height/width
|
// Responsive image size based on available height/width
|
||||||
final double maxImageHeight = constraints.maxHeight * 0.9;
|
final double maxImageHeight = constraints.maxHeight * 0.9;
|
||||||
final double maxImageWidth = constraints.maxWidth * 0.9;
|
final double maxImageWidth = constraints.maxWidth * 0.9;
|
||||||
final double imageSize =
|
final double imageSize =
|
||||||
maxImageHeight < maxImageWidth
|
maxImageHeight < maxImageWidth
|
||||||
? maxImageHeight
|
? maxImageHeight
|
||||||
: maxImageWidth;
|
: maxImageWidth;
|
||||||
|
|
||||||
return Column(
|
return SizedBox(
|
||||||
mainAxisSize: MainAxisSize.min,
|
height: imageSize,
|
||||||
children: [
|
width: imageSize,
|
||||||
// Role image
|
child: SvgPicture.asset(
|
||||||
SizedBox(
|
isOfficer
|
||||||
height: imageSize,
|
? (isDark
|
||||||
width: imageSize,
|
? TImages.communicationDark
|
||||||
child: SvgPicture.asset(
|
: TImages.communication)
|
||||||
isOfficer
|
: (isDark ? TImages.fallingDark : TImages.falling),
|
||||||
? (isDark
|
fit: BoxFit.contain,
|
||||||
? TImages.communicationDark
|
|
||||||
: TImages.communication)
|
|
||||||
: (isDark ? TImages.fallingDark : TImages.falling),
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Back button in the app bar
|
||||||
|
leading: Padding(
|
||||||
|
padding: EdgeInsets.only(top: topPadding * 0.2),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => Get.back(),
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(left: TSizes.sm),
|
||||||
|
padding: const EdgeInsets.all(TSizes.xs),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -172,15 +172,24 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
) {
|
) {
|
||||||
final isDark = THelperFunctions.isDarkMode(context);
|
final isDark = THelperFunctions.isDarkMode(context);
|
||||||
|
|
||||||
return Padding(
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? TColors.dark : TColors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 5,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
padding: const EdgeInsets.fromLTRB(
|
padding: const EdgeInsets.fromLTRB(
|
||||||
TSizes.defaultSpace,
|
TSizes.defaultSpace,
|
||||||
TSizes.md,
|
TSizes.xs,
|
||||||
TSizes.defaultSpace,
|
TSizes.defaultSpace,
|
||||||
0,
|
TSizes.xs,
|
||||||
),
|
),
|
||||||
child: Container(
|
child: Container(
|
||||||
// Increase height from 50 to 60 or more
|
|
||||||
height: 60,
|
height: 60,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDark ? TColors.darkContainer : TColors.lightContainer,
|
color: isDark ? TColors.darkContainer : TColors.lightContainer,
|
||||||
|
@ -425,6 +434,9 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// Add extra space at bottom for safety
|
||||||
|
SizedBox(height: MediaQuery.of(context).padding.bottom + 16),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -486,3 +498,5 @@ class SignupWithRoleScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,427 +0,0 @@
|
||||||
// import 'package:flutter/material.dart';
|
|
||||||
// import 'package:flutter/services.dart';
|
|
||||||
// import 'package:get/get.dart';
|
|
||||||
// import 'package:sigap/src/features/auth/presentasion/controllers/registration_form_controller.dart';
|
|
||||||
// import 'package:sigap/src/features/auth/presentasion/widgets/auth_button.dart';
|
|
||||||
// import 'package:sigap/src/features/personalization/data/models/index.dart';
|
|
||||||
// import 'package:sigap/src/shared/widgets/dropdown/custom_dropdown.dart';
|
|
||||||
// import 'package:sigap/src/shared/widgets/indicators/step_indicator/index.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/validators/validation.dart';
|
|
||||||
|
|
||||||
// class FormRegistrationScreen extends StatelessWidget {
|
|
||||||
// const FormRegistrationScreen({super.key});
|
|
||||||
|
|
||||||
// @override
|
|
||||||
// Widget build(BuildContext context) {
|
|
||||||
// // Get the controller
|
|
||||||
// final controller = Get.find<FormRegistrationController>();
|
|
||||||
|
|
||||||
// // Set system overlay style
|
|
||||||
// SystemChrome.setSystemUIOverlayStyle(
|
|
||||||
// const SystemUiOverlayStyle(
|
|
||||||
// statusBarColor: Colors.transparent,
|
|
||||||
// statusBarIconBrightness: Brightness.dark,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// return Scaffold(
|
|
||||||
// backgroundColor: TColors.light,
|
|
||||||
// appBar: AppBar(
|
|
||||||
// backgroundColor: Colors.transparent,
|
|
||||||
// elevation: 0,
|
|
||||||
// title: Obx(
|
|
||||||
// () => Text(
|
|
||||||
// 'Complete Your ${controller.selectedRole.value?.name ?? ""} Profile',
|
|
||||||
// style: TextStyle(
|
|
||||||
// color: TColors.textPrimary,
|
|
||||||
// fontWeight: FontWeight.bold,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// centerTitle: true,
|
|
||||||
// leading: IconButton(
|
|
||||||
// icon: Icon(Icons.arrow_back, color: TColors.textPrimary),
|
|
||||||
// onPressed: () => Get.back(),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// body: Obx(() {
|
|
||||||
// // Show loading while initializing
|
|
||||||
// if (controller.selectedRole.value == null) {
|
|
||||||
// return const Center(child: CircularProgressIndicator());
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return SafeArea(
|
|
||||||
// child: Column(
|
|
||||||
// children: [
|
|
||||||
// // Step indicator
|
|
||||||
// Padding(
|
|
||||||
// padding: const EdgeInsets.all(24.0),
|
|
||||||
// child: Obx(
|
|
||||||
// () => StepIndicator(
|
|
||||||
// currentStep: controller.currentStep.value,
|
|
||||||
// totalSteps: controller.stepFormKeys.length,
|
|
||||||
// stepTitles: _getStepTitles(controller.selectedRole.value!),
|
|
||||||
// onStepTapped: controller.goToStep,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Step content
|
|
||||||
// Expanded(
|
|
||||||
// child: SingleChildScrollView(
|
|
||||||
// child: Padding(
|
|
||||||
// padding: const EdgeInsets.all(24.0),
|
|
||||||
// child: Obx(() {
|
|
||||||
// return _buildStepContent(controller);
|
|
||||||
// }),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Navigation buttons
|
|
||||||
// Padding(
|
|
||||||
// padding: const EdgeInsets.all(24.0),
|
|
||||||
// child: Row(
|
|
||||||
// children: [
|
|
||||||
// // Back button
|
|
||||||
// Obx(
|
|
||||||
// () =>
|
|
||||||
// controller.currentStep.value > 0
|
|
||||||
// ? Expanded(
|
|
||||||
// child: Padding(
|
|
||||||
// padding: const EdgeInsets.only(right: 8.0),
|
|
||||||
// child: AuthButton(
|
|
||||||
// text: 'Previous',
|
|
||||||
// onPressed: controller.previousStep,
|
|
||||||
// isPrimary: false,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// : const SizedBox.shrink(),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Next/Submit button
|
|
||||||
// Expanded(
|
|
||||||
// child: Padding(
|
|
||||||
// padding: EdgeInsets.only(
|
|
||||||
// left: controller.currentStep.value > 0 ? 8.0 : 0.0,
|
|
||||||
// ),
|
|
||||||
// child: Obx(
|
|
||||||
// () => AuthButton(
|
|
||||||
// text:
|
|
||||||
// controller.currentStep.value ==
|
|
||||||
// controller.stepFormKeys.length - 1
|
|
||||||
// ? 'Submit'
|
|
||||||
// : 'Next',
|
|
||||||
// onPressed: controller.nextStep,
|
|
||||||
// isLoading: controller.isLoading.value,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// List<String> _getStepTitles(RoleModel role) {
|
|
||||||
// if (role.isOfficer) {
|
|
||||||
// return ['Personal', 'Officer Info', 'Unit Info'];
|
|
||||||
// } else {
|
|
||||||
// return ['Personal', 'Emergency'];
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildStepContent(FormRegistrationController controller) {
|
|
||||||
// final isOfficer = controller.selectedRole.value?.isOfficer ?? false;
|
|
||||||
|
|
||||||
// switch (controller.currentStep.value) {
|
|
||||||
// case 0:
|
|
||||||
// return _buildPersonalInfoStep(controller);
|
|
||||||
// case 1:
|
|
||||||
// return isOfficer
|
|
||||||
// ? _buildOfficerInfoStep(controller)
|
|
||||||
// : _buildEmergencyContactStep(controller);
|
|
||||||
// case 2:
|
|
||||||
// // This step only exists for officers
|
|
||||||
// if (isOfficer) {
|
|
||||||
// return _buildOfficerAdditionalInfoStep(controller);
|
|
||||||
// }
|
|
||||||
// return const SizedBox.shrink();
|
|
||||||
// default:
|
|
||||||
// return const SizedBox.shrink();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildPersonalInfoStep(FormRegistrationController controller) {
|
|
||||||
// return Form(
|
|
||||||
// key: controller.stepFormKeys[0],
|
|
||||||
// child: Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// Text(
|
|
||||||
// 'Personal Information',
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 20,
|
|
||||||
// fontWeight: FontWeight.bold,
|
|
||||||
// color: TColors.textPrimary,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 8),
|
|
||||||
// Text(
|
|
||||||
// 'Please provide your personal details',
|
|
||||||
// style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// // First Name field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'First Name',
|
|
||||||
// controller: controller.firstNameController,
|
|
||||||
// validator:
|
|
||||||
// (value) =>
|
|
||||||
// TValidators.validateUserInput('First name', value, 50),
|
|
||||||
// errorText: controller.firstNameError.value,
|
|
||||||
// textInputAction: TextInputAction.next,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Last Name field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'Last Name',
|
|
||||||
// controller: controller.lastNameController,
|
|
||||||
// validator:
|
|
||||||
// (value) => TValidators.validateUserInput(
|
|
||||||
// 'Last name',
|
|
||||||
// value,
|
|
||||||
// 50,
|
|
||||||
// required: false,
|
|
||||||
// ),
|
|
||||||
// errorText: controller.lastNameError.value,
|
|
||||||
// textInputAction: TextInputAction.next,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Phone field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'Phone Number',
|
|
||||||
// controller: controller.phoneController,
|
|
||||||
// validator: TValidators.validatePhoneNumber,
|
|
||||||
// errorText: controller.phoneError.value,
|
|
||||||
// keyboardType: TextInputType.phone,
|
|
||||||
// textInputAction: TextInputAction.next,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Address field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'Address',
|
|
||||||
// controller: controller.addressController,
|
|
||||||
// validator:
|
|
||||||
// (value) =>
|
|
||||||
// TValidators.validateUserInput('Address', value, 255),
|
|
||||||
// errorText: controller.addressError.value,
|
|
||||||
// textInputAction: TextInputAction.done,
|
|
||||||
// maxLines: 3,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildEmergencyContactStep(FormRegistrationController controller) {
|
|
||||||
// return Form(
|
|
||||||
// key: controller.stepFormKeys[1],
|
|
||||||
// child: Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// Text(
|
|
||||||
// 'Additional Information',
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 20,
|
|
||||||
// fontWeight: FontWeight.bold,
|
|
||||||
// color: TColors.textPrimary,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 8),
|
|
||||||
// Text(
|
|
||||||
// 'Please provide additional personal details',
|
|
||||||
// style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// // NIK field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'NIK (Identity Number)',
|
|
||||||
// controller: controller.nikController,
|
|
||||||
// validator:
|
|
||||||
// (value) => TValidators.validateUserInput('NIK', value, 16),
|
|
||||||
// errorText: controller.nikError.value,
|
|
||||||
// textInputAction: TextInputAction.next,
|
|
||||||
// keyboardType: TextInputType.number,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Bio field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'Bio',
|
|
||||||
// controller: controller.bioController,
|
|
||||||
// validator:
|
|
||||||
// (value) => TValidators.validateUserInput(
|
|
||||||
// 'Bio',
|
|
||||||
// value,
|
|
||||||
// 255,
|
|
||||||
// required: false,
|
|
||||||
// ),
|
|
||||||
// errorText: controller.bioError.value,
|
|
||||||
// textInputAction: TextInputAction.next,
|
|
||||||
// maxLines: 3,
|
|
||||||
// hintText: 'Tell us a little about yourself (optional)',
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Birth Date field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'Birth Date (YYYY-MM-DD)',
|
|
||||||
// controller: controller.birthDateController,
|
|
||||||
// validator:
|
|
||||||
// (value) =>
|
|
||||||
// TValidators.validateUserInput('Birth date', value, 10),
|
|
||||||
// errorText: controller.birthDateError.value,
|
|
||||||
// textInputAction: TextInputAction.done,
|
|
||||||
// keyboardType: TextInputType.datetime,
|
|
||||||
// hintText: 'e.g., 1990-01-31',
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildOfficerInfoStep(FormRegistrationController controller) {
|
|
||||||
// return Form(
|
|
||||||
// key: controller.stepFormKeys[1],
|
|
||||||
// child: Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// Text(
|
|
||||||
// 'Officer Information',
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 20,
|
|
||||||
// fontWeight: FontWeight.bold,
|
|
||||||
// color: TColors.textPrimary,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 8),
|
|
||||||
// Text(
|
|
||||||
// 'Please provide your officer details',
|
|
||||||
// style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// // NRP field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'NRP',
|
|
||||||
// controller: controller.nrpController,
|
|
||||||
// validator: TValidators.validateNRP,
|
|
||||||
// errorText: controller.nrpError.value,
|
|
||||||
// textInputAction: TextInputAction.next,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Rank field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'Rank',
|
|
||||||
// controller: controller.rankController,
|
|
||||||
// validator: TValidators.validateRank,
|
|
||||||
// errorText: controller.rankError.value,
|
|
||||||
// textInputAction: TextInputAction.done,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Widget _buildOfficerAdditionalInfoStep(
|
|
||||||
// FormRegistrationController controller,
|
|
||||||
// ) {
|
|
||||||
// return Form(
|
|
||||||
// key: controller.stepFormKeys[2],
|
|
||||||
// child: Column(
|
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
// children: [
|
|
||||||
// Text(
|
|
||||||
// 'Unit Information',
|
|
||||||
// style: TextStyle(
|
|
||||||
// fontSize: 20,
|
|
||||||
// fontWeight: FontWeight.bold,
|
|
||||||
// color: TColors.textPrimary,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 8),
|
|
||||||
// Text(
|
|
||||||
// 'Please provide your unit details',
|
|
||||||
// style: TextStyle(fontSize: 14, color: TColors.textSecondary),
|
|
||||||
// ),
|
|
||||||
// const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// // Position field
|
|
||||||
// Obx(
|
|
||||||
// () => CustomTextField(
|
|
||||||
// label: 'Position',
|
|
||||||
// controller: controller.positionController,
|
|
||||||
// validator: TValidators.validatePosition,
|
|
||||||
// errorText: controller.positionError.value,
|
|
||||||
// textInputAction: TextInputAction.next,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
|
|
||||||
// // Unit Dropdown
|
|
||||||
// Obx(
|
|
||||||
// () => CustomDropdown(
|
|
||||||
// label: 'Unit',
|
|
||||||
// items:
|
|
||||||
// controller.availableUnits
|
|
||||||
// .map(
|
|
||||||
// (unit) => DropdownMenuItem(
|
|
||||||
// value: unit.codeUnit,
|
|
||||||
// child: Text(unit.name),
|
|
||||||
// ),
|
|
||||||
// )
|
|
||||||
// .toList(),
|
|
||||||
// value:
|
|
||||||
// controller.unitIdController.text.isEmpty
|
|
||||||
// ? null
|
|
||||||
// : controller.unitIdController.text,
|
|
||||||
// onChanged: (value) {
|
|
||||||
// if (value != null) {
|
|
||||||
// controller.unitIdController.text = value.toString();
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// validator: (value) => TValidators.validateUnitId(value),
|
|
||||||
// errorText: controller.unitIdError.value,
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// ],
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -46,59 +46,63 @@ class PasswordField extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.sm),
|
const SizedBox(height: TSizes.sm),
|
||||||
TextFormField(
|
Obx(
|
||||||
controller: controller,
|
() =>
|
||||||
validator: validator,
|
TextFormField(
|
||||||
obscureText: !isVisible.value,
|
controller: controller,
|
||||||
textInputAction: textInputAction,
|
validator: validator,
|
||||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
obscureText: !isVisible.value,
|
||||||
color: isDark ? TColors.white : TColors.textPrimary,
|
textInputAction: textInputAction,
|
||||||
),
|
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||||
decoration: InputDecoration(
|
color: isDark ? TColors.white : TColors.textPrimary,
|
||||||
hintText: hintText,
|
|
||||||
hintStyle: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
|
|
||||||
errorText:
|
|
||||||
errorText != null && errorText!.isNotEmpty ? errorText : null,
|
|
||||||
errorStyle: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodySmall?.copyWith(color: TColors.error),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: TSizes.md,
|
|
||||||
vertical: TSizes.md,
|
|
||||||
),
|
),
|
||||||
prefixIcon: prefixIcon,
|
decoration: InputDecoration(
|
||||||
suffixIcon: IconButton(
|
hintText: hintText,
|
||||||
onPressed: onToggleVisibility,
|
hintStyle: Theme.of(
|
||||||
icon: Icon(
|
context,
|
||||||
isVisible.value ? Icons.visibility_off : Icons.visibility,
|
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
|
||||||
color: effectiveAccentColor,
|
errorText:
|
||||||
semanticLabel:
|
errorText != null && errorText!.isNotEmpty ? errorText : null,
|
||||||
isVisible.value ? 'Hide password' : 'Show password',
|
errorStyle: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall?.copyWith(color: TColors.error),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: TSizes.md,
|
||||||
|
vertical: TSizes.md,
|
||||||
|
),
|
||||||
|
prefixIcon: prefixIcon,
|
||||||
|
suffixIcon: IconButton(
|
||||||
|
onPressed: onToggleVisibility,
|
||||||
|
icon: Icon(
|
||||||
|
isVisible.value ? Icons.visibility_off : Icons.visibility,
|
||||||
|
color: effectiveAccentColor,
|
||||||
|
semanticLabel:
|
||||||
|
isVisible.value ? 'Hide password' : 'Show password',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor:
|
||||||
|
isDark ? TColors.darkContainer : TColors.lightContainer,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
|
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
|
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
|
borderSide: BorderSide(color: effectiveAccentColor, width: 1.5),
|
||||||
|
),
|
||||||
|
errorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
|
borderSide: BorderSide(color: TColors.error, width: 1),
|
||||||
|
),
|
||||||
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
||||||
|
borderSide: BorderSide(color: TColors.error, width: 1.5),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
filled: true,
|
|
||||||
fillColor: isDark ? TColors.darkContainer : TColors.lightContainer,
|
|
||||||
border: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
|
||||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
|
||||||
),
|
|
||||||
enabledBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
|
||||||
borderSide: BorderSide(color: TColors.borderPrimary, width: 1),
|
|
||||||
),
|
|
||||||
focusedBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
|
||||||
borderSide: BorderSide(color: effectiveAccentColor, width: 1.5),
|
|
||||||
),
|
|
||||||
errorBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
|
||||||
borderSide: BorderSide(color: TColors.error, width: 1),
|
|
||||||
),
|
|
||||||
focusedErrorBorder: OutlineInputBorder(
|
|
||||||
borderRadius: BorderRadius.circular(TSizes.inputFieldRadius),
|
|
||||||
borderSide: BorderSide(color: TColors.error, width: 1.5),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -112,10 +112,13 @@ class OnboardingController extends GetxController
|
||||||
// If location is valid, proceed to role selection
|
// If location is valid, proceed to role selection
|
||||||
Get.offAllNamed(AppRoutes.signupWithRole);
|
Get.offAllNamed(AppRoutes.signupWithRole);
|
||||||
|
|
||||||
TLoaders.successSnackBar(
|
// TLoaders.successSnackBar(
|
||||||
title: 'Location Valid',
|
// title: 'Location Valid',
|
||||||
message: 'Checking location was successful',
|
// message: 'Checking location was successful',
|
||||||
);
|
// );
|
||||||
|
|
||||||
|
// Store isfirstTime to false in storage
|
||||||
|
_storage.write('isFirstTime', false);
|
||||||
} else {
|
} else {
|
||||||
// If location is invalid, show warning screen
|
// If location is invalid, show warning screen
|
||||||
Get.offAllNamed(AppRoutes.locationWarning);
|
Get.offAllNamed(AppRoutes.locationWarning);
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:sigap/src/cores/services/location_service.dart';
|
import 'package:sigap/src/cores/services/location_service.dart';
|
||||||
import 'package:sigap/src/utils/constants/app_routes.dart';
|
import 'package:sigap/src/utils/constants/app_routes.dart';
|
||||||
|
import 'package:sigap/src/utils/constants/image_strings.dart';
|
||||||
import 'package:sigap/src/utils/constants/sizes.dart';
|
import 'package:sigap/src/utils/constants/sizes.dart';
|
||||||
|
|
||||||
class LocationWarningScreen extends StatelessWidget {
|
class LocationWarningScreen extends StatelessWidget {
|
||||||
|
@ -27,10 +29,14 @@ class LocationWarningScreen extends StatelessWidget {
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Warning Icon - use theme error color
|
// Warning Icon - use theme error color
|
||||||
Icon(
|
Lottie.asset(
|
||||||
Icons.location_off,
|
TImages.unverifyLocationAnimation,
|
||||||
size: 80,
|
width: 200,
|
||||||
color: theme.colorScheme.error,
|
height: 200,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
return const Icon(Icons.error, size: 100, color: Colors.red);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: TSizes.spaceBtwSections),
|
const SizedBox(height: TSizes.spaceBtwSections),
|
||||||
|
|
|
@ -9,7 +9,6 @@ import '../../controllers/onboarding_controller.dart';
|
||||||
|
|
||||||
class OnboardingScreen extends StatelessWidget {
|
class OnboardingScreen extends StatelessWidget {
|
||||||
const OnboardingScreen({super.key});
|
const OnboardingScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Get the controller
|
// Get the controller
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Custom SliverPersistentHeaderDelegate for the tab bar
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class TSliverTabBarDelegate extends SliverPersistentHeaderDelegate {
|
||||||
|
final Widget child;
|
||||||
|
final double minHeight;
|
||||||
|
final double maxHeight;
|
||||||
|
|
||||||
|
TSliverTabBarDelegate({
|
||||||
|
required this.child,
|
||||||
|
required this.minHeight,
|
||||||
|
required this.maxHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get minExtent => minHeight;
|
||||||
|
|
||||||
|
@override
|
||||||
|
double get maxExtent => maxHeight;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(
|
||||||
|
BuildContext context,
|
||||||
|
double shrinkOffset,
|
||||||
|
bool overlapsContent,
|
||||||
|
) {
|
||||||
|
return SizedBox.expand(child: child);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRebuild(TSliverTabBarDelegate oldDelegate) {
|
||||||
|
return maxHeight != oldDelegate.maxHeight ||
|
||||||
|
minHeight != oldDelegate.minHeight ||
|
||||||
|
child != oldDelegate.child;
|
||||||
|
}
|
||||||
|
}
|
|
@ -72,9 +72,6 @@ class CustomTextField extends StatelessWidget {
|
||||||
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
|
).textTheme.bodyMedium?.copyWith(color: TColors.textSecondary),
|
||||||
errorText:
|
errorText:
|
||||||
errorText != null && errorText!.isNotEmpty ? errorText : null,
|
errorText != null && errorText!.isNotEmpty ? errorText : null,
|
||||||
errorStyle: Theme.of(
|
|
||||||
context,
|
|
||||||
).textTheme.bodySmall?.copyWith(color: TColors.error),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: TSizes.md,
|
horizontal: TSizes.md,
|
||||||
vertical: TSizes.md,
|
vertical: TSizes.md,
|
||||||
|
@ -105,7 +102,17 @@ class CustomTextField extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: TSizes.spaceBtwInputFields),
|
// if (errorText != null && errorText!.isNotEmpty)
|
||||||
|
// Padding(
|
||||||
|
// padding: const EdgeInsets.only(top: 6.0),
|
||||||
|
// child: Text(
|
||||||
|
// errorText!,
|
||||||
|
// style: Theme.of(
|
||||||
|
// context,
|
||||||
|
// ).textTheme.bodySmall?.copyWith(color: TColors.error),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// const SizedBox(height: TSizes.spaceBtwInputFields),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// Class containing all the FormKey identifiers used across the app
|
||||||
|
/// This helps prevent duplication of form key strings and makes it easier to manage them
|
||||||
|
class TGlobalFormKey {
|
||||||
|
// Registration form step keys
|
||||||
|
static const String personalInfoForm = 'personal_info_new_form_key';
|
||||||
|
static const String idCardVerificationForm = 'id_card_verification_form_key';
|
||||||
|
static const String selfieVerificationForm = 'selfie_verification_form_key';
|
||||||
|
static const String identityVerificationForm =
|
||||||
|
'identity_verification_form_key';
|
||||||
|
static const String officerInfoForm = 'officer_info_form_key';
|
||||||
|
static const String unitInfoForm = 'unit_info_form_key';
|
||||||
|
|
||||||
|
// Authentication form keys
|
||||||
|
static const String signInForm = 'sign_in_form_key';
|
||||||
|
static const String signUpForm = 'sign_up_form_key';
|
||||||
|
static const String forgotPasswordForm = 'forgot_password_form_key';
|
||||||
|
static const String resetPasswordForm = 'reset_password_form_key';
|
||||||
|
static const String otpVerificationForm = 'otp_verification_form_key';
|
||||||
|
|
||||||
|
// Profile form keys
|
||||||
|
static const String editProfileForm = 'edit_profile_form_key';
|
||||||
|
static const String changePasswordForm = 'change_password_form_key';
|
||||||
|
static const String notificationSettingsForm =
|
||||||
|
'notification_settings_form_key';
|
||||||
|
|
||||||
|
// Create global keys for forms with specific debug labels
|
||||||
|
static GlobalKey<FormState> createFormKey(String keyName) {
|
||||||
|
return GlobalKey<FormState>(debugLabel: keyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get a personal info form key
|
||||||
|
static GlobalKey<FormState> personalInfo() {
|
||||||
|
return createFormKey(personalInfoForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get an ID card verification form key
|
||||||
|
static GlobalKey<FormState> idCardVerification() {
|
||||||
|
return createFormKey(idCardVerificationForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get a selfie verification form key
|
||||||
|
static GlobalKey<FormState> selfieVerification() {
|
||||||
|
return createFormKey(selfieVerificationForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get an identity verification form key
|
||||||
|
static GlobalKey<FormState> identityVerification() {
|
||||||
|
return createFormKey(identityVerificationForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get an officer info form key
|
||||||
|
static GlobalKey<FormState> officerInfo() {
|
||||||
|
return createFormKey(officerInfoForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get a unit info form key
|
||||||
|
static GlobalKey<FormState> unitInfo() {
|
||||||
|
return createFormKey(unitInfoForm);
|
||||||
|
}
|
||||||
|
}
|
|
@ -174,6 +174,8 @@ class TImages {
|
||||||
"assets/images/animations/splash-dark.json";
|
"assets/images/animations/splash-dark.json";
|
||||||
static const String splashLightAnimation =
|
static const String splashLightAnimation =
|
||||||
"assets/images/animations/splash-light.json";
|
"assets/images/animations/splash-light.json";
|
||||||
|
static const String unverifyLocationAnimation =
|
||||||
|
"assets/images/animations/empty-address.json";
|
||||||
|
|
||||||
// -- Content Images (assets/images/content)
|
// -- Content Images (assets/images/content)
|
||||||
static const String backpackingDark =
|
static const String backpackingDark =
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
class DNum {
|
class TNum {
|
||||||
// Auth Number
|
// Auth Number
|
||||||
static const int oneTimePassword = 6;
|
static const int oneTimePassword = 6;
|
||||||
|
static const int totalStepViewer = 4;
|
||||||
|
static const int totalStepOfficer = 5;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,81 +9,104 @@ class TLoaders {
|
||||||
static hideSnackBar() => ScaffoldMessenger.of(Get.context!).hideCurrentSnackBar();
|
static hideSnackBar() => ScaffoldMessenger.of(Get.context!).hideCurrentSnackBar();
|
||||||
|
|
||||||
static customToast({required message}) {
|
static customToast({required message}) {
|
||||||
ScaffoldMessenger.of(Get.context!).showSnackBar(
|
// Use post-frame callback for any UI updates
|
||||||
SnackBar(
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
elevation: 0,
|
ScaffoldMessenger.of(Get.context!).showSnackBar(
|
||||||
duration: const Duration(seconds: 3),
|
SnackBar(
|
||||||
backgroundColor: Colors.transparent,
|
elevation: 0,
|
||||||
content: Container(
|
duration: const Duration(seconds: 3),
|
||||||
padding: const EdgeInsets.all(12.0),
|
backgroundColor: Colors.transparent,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 30),
|
content: Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(12.0),
|
||||||
borderRadius: BorderRadius.circular(30),
|
margin: const EdgeInsets.symmetric(horizontal: 30),
|
||||||
color: THelperFunctions.isDarkMode(Get.context!) ? TColors.darkerGrey.withOpacity(0.9) : TColors.grey.withOpacity(0.9),
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
color:
|
||||||
|
THelperFunctions.isDarkMode(Get.context!)
|
||||||
|
? TColors.darkerGrey.withOpacity(0.9)
|
||||||
|
: TColors.grey.withOpacity(0.9),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
message,
|
||||||
|
style: Theme.of(Get.context!).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Center(child: Text(message, style: Theme.of(Get.context!).textTheme.labelLarge)),
|
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static successSnackBar({required title, message = '', duration = 3}) {
|
static successSnackBar({required title, message = '', duration = 3}) {
|
||||||
Get.snackbar(
|
// Use post-frame callback for any UI updates
|
||||||
title,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
message,
|
Get.snackbar(
|
||||||
isDismissible: true,
|
title,
|
||||||
shouldIconPulse: true,
|
message,
|
||||||
colorText: Colors.white,
|
isDismissible: true,
|
||||||
backgroundColor: TColors.primary,
|
shouldIconPulse: true,
|
||||||
snackPosition: SnackPosition.TOP,
|
colorText: Colors.white,
|
||||||
duration: Duration(seconds: duration),
|
backgroundColor: TColors.primary,
|
||||||
margin: const EdgeInsets.all(10),
|
snackPosition: SnackPosition.TOP,
|
||||||
icon: const Icon(Iconsax.check, color: TColors.white),
|
duration: Duration(seconds: duration),
|
||||||
);
|
margin: const EdgeInsets.all(10),
|
||||||
|
icon: const Icon(Iconsax.check, color: TColors.white),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static warningSnackBar({required title, message = ''}) {
|
static warningSnackBar({required title, message = ''}) {
|
||||||
Get.snackbar(
|
// Use post-frame callback for any UI updates
|
||||||
title,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
message,
|
Get.snackbar(
|
||||||
isDismissible: true,
|
title,
|
||||||
shouldIconPulse: true,
|
message,
|
||||||
colorText: TColors.white,
|
isDismissible: true,
|
||||||
backgroundColor: Colors.orange,
|
shouldIconPulse: true,
|
||||||
snackPosition: SnackPosition.TOP,
|
colorText: TColors.white,
|
||||||
duration: const Duration(seconds: 3),
|
backgroundColor: Colors.orange,
|
||||||
margin: const EdgeInsets.all(20),
|
snackPosition: SnackPosition.TOP,
|
||||||
icon: const Icon(Iconsax.warning_2, color: TColors.white),
|
duration: const Duration(seconds: 3),
|
||||||
);
|
margin: const EdgeInsets.all(20),
|
||||||
|
icon: const Icon(Iconsax.warning_2, color: TColors.white),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static errorSnackBar({required title, message = ''}) {
|
static errorSnackBar({required title, message = ''}) {
|
||||||
Get.snackbar(
|
// Use post-frame callback for any UI updates
|
||||||
title,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
message,
|
Get.snackbar(
|
||||||
isDismissible: true,
|
title,
|
||||||
shouldIconPulse: true,
|
message,
|
||||||
colorText: TColors.white,
|
isDismissible: true,
|
||||||
backgroundColor: Colors.red.shade600,
|
shouldIconPulse: true,
|
||||||
snackPosition: SnackPosition.TOP,
|
colorText: TColors.white,
|
||||||
duration: const Duration(seconds: 3),
|
backgroundColor: Colors.red.shade600,
|
||||||
margin: const EdgeInsets.all(20),
|
snackPosition: SnackPosition.TOP,
|
||||||
icon: const Icon(Iconsax.warning_2, color: TColors.white),
|
duration: const Duration(seconds: 3),
|
||||||
);
|
margin: const EdgeInsets.all(20),
|
||||||
|
icon: const Icon(Iconsax.warning_2, color: TColors.white),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static infoSnackBar({required title, message = ''}) {
|
static infoSnackBar({required title, message = ''}) {
|
||||||
Get.snackbar(
|
// Use post-frame callback for any UI updates
|
||||||
title,
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
message,
|
Get.snackbar(
|
||||||
isDismissible: true,
|
title,
|
||||||
shouldIconPulse: true,
|
message,
|
||||||
colorText: TColors.white,
|
isDismissible: true,
|
||||||
backgroundColor: Colors.blue.shade600,
|
shouldIconPulse: true,
|
||||||
snackPosition: SnackPosition.TOP,
|
colorText: TColors.white,
|
||||||
duration: const Duration(seconds: 3),
|
backgroundColor: Colors.blue.shade600,
|
||||||
margin: const EdgeInsets.all(20),
|
snackPosition: SnackPosition.TOP,
|
||||||
icon: const Icon(Iconsax.info_circle, color: TColors.white),
|
duration: const Duration(seconds: 3),
|
||||||
);
|
margin: const EdgeInsets.all(20),
|
||||||
|
icon: const Icon(Iconsax.info_circle, color: TColors.white),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ datasource db {
|
||||||
model profiles {
|
model profiles {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
user_id String @unique @db.Uuid
|
user_id String @unique @db.Uuid
|
||||||
nik String @unique @default("") @db.VarChar(100)
|
|
||||||
avatar String? @db.VarChar(355)
|
avatar String? @db.VarChar(355)
|
||||||
username String? @unique @db.VarChar(255)
|
username String? @unique @db.VarChar(255)
|
||||||
first_name String? @db.VarChar(255)
|
first_name String? @db.VarChar(255)
|
||||||
|
@ -21,9 +20,9 @@ model profiles {
|
||||||
bio String? @db.VarChar
|
bio String? @db.VarChar
|
||||||
address Json? @db.Json
|
address Json? @db.Json
|
||||||
birth_date DateTime?
|
birth_date DateTime?
|
||||||
|
nik String? @default("") @db.VarChar(100)
|
||||||
users users @relation(fields: [user_id], references: [id])
|
users users @relation(fields: [user_id], references: [id])
|
||||||
|
|
||||||
@@index([nik], map: "idx_profiles_nik")
|
|
||||||
@@index([user_id])
|
@@index([user_id])
|
||||||
@@index([username])
|
@@index([username])
|
||||||
}
|
}
|
||||||
|
@ -43,19 +42,19 @@ model users {
|
||||||
user_metadata Json?
|
user_metadata Json?
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||||
is_banned Boolean @default(false)
|
|
||||||
spoofing_attempts Int @default(0)
|
|
||||||
panic_strike Int @default(0)
|
|
||||||
banned_reason String? @db.VarChar(255)
|
|
||||||
banned_until DateTime? @db.Timestamptz(6)
|
banned_until DateTime? @db.Timestamptz(6)
|
||||||
is_anonymous Boolean @default(false)
|
is_anonymous Boolean @default(false)
|
||||||
|
banned_reason String? @db.VarChar(255)
|
||||||
|
is_banned Boolean @default(false)
|
||||||
|
panic_strike Int @default(0)
|
||||||
|
spoofing_attempts Int @default(0)
|
||||||
events events[]
|
events events[]
|
||||||
incident_logs incident_logs[]
|
incident_logs incident_logs[]
|
||||||
location_logs location_logs[]
|
location_logs location_logs[]
|
||||||
|
panic_button_logs panic_button_logs[]
|
||||||
profile profiles?
|
profile profiles?
|
||||||
sessions sessions[]
|
sessions sessions[]
|
||||||
role roles @relation(fields: [roles_id], references: [id])
|
role roles @relation(fields: [roles_id], references: [id])
|
||||||
panic_button_logs panic_button_logs[]
|
|
||||||
|
|
||||||
@@index([is_anonymous])
|
@@index([is_anonymous])
|
||||||
@@index([created_at])
|
@@index([created_at])
|
||||||
|
@ -68,9 +67,9 @@ model roles {
|
||||||
description String?
|
description String?
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
officers officers[]
|
||||||
permissions permissions[]
|
permissions permissions[]
|
||||||
users users[]
|
users users[]
|
||||||
officers officers[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model sessions {
|
model sessions {
|
||||||
|
@ -267,10 +266,10 @@ model incident_logs {
|
||||||
verified Boolean? @default(false)
|
verified Boolean? @default(false)
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
|
evidence evidence[]
|
||||||
crime_categories crime_categories @relation(fields: [category_id], references: [id], map: "fk_incident_category")
|
crime_categories crime_categories @relation(fields: [category_id], references: [id], map: "fk_incident_category")
|
||||||
locations locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
locations locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
user users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
user users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
evidence evidence[]
|
|
||||||
panic_button_logs panic_button_logs[]
|
panic_button_logs panic_button_logs[]
|
||||||
|
|
||||||
@@index([category_id], map: "idx_incident_logs_category_id")
|
@@index([category_id], map: "idx_incident_logs_category_id")
|
||||||
|
@ -278,16 +277,15 @@ model incident_logs {
|
||||||
}
|
}
|
||||||
|
|
||||||
model evidence {
|
model evidence {
|
||||||
id String @id @unique @db.VarChar(20)
|
incident_id String @db.Uuid
|
||||||
incident_id String @db.Uuid
|
type String @db.VarChar(50)
|
||||||
type String @db.VarChar(50) // contoh: photo, video, document, images
|
url String
|
||||||
url String @db.Text
|
uploaded_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
description String? @db.VarChar(255)
|
caption String? @db.VarChar(255)
|
||||||
caption String? @db.VarChar(255)
|
description String? @db.VarChar(255)
|
||||||
metadata Json?
|
metadata Json?
|
||||||
uploaded_at DateTime? @default(now()) @db.Timestamptz(6)
|
id String @id @unique @db.VarChar(20)
|
||||||
|
incident incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade)
|
||||||
incident incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade)
|
|
||||||
|
|
||||||
@@index([incident_id], map: "idx_evidence_incident_id")
|
@@index([incident_id], map: "idx_evidence_incident_id")
|
||||||
}
|
}
|
||||||
|
@ -307,11 +305,11 @@ model units {
|
||||||
location Unsupported("geography")
|
location Unsupported("geography")
|
||||||
city_id String @db.VarChar(20)
|
city_id String @db.VarChar(20)
|
||||||
phone String? @db.VarChar(20)
|
phone String? @db.VarChar(20)
|
||||||
|
officers officers[]
|
||||||
|
patrol_units patrol_units[]
|
||||||
unit_statistics unit_statistics[]
|
unit_statistics unit_statistics[]
|
||||||
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
cities cities @relation(fields: [city_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
districts districts? @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
districts districts? @relation(fields: [district_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
officers officers[]
|
|
||||||
patrol_units patrol_units[]
|
|
||||||
|
|
||||||
@@index([name], map: "idx_units_name")
|
@@index([name], map: "idx_units_name")
|
||||||
@@index([type], map: "idx_units_type")
|
@@index([type], map: "idx_units_type")
|
||||||
|
@ -320,24 +318,20 @@ model units {
|
||||||
@@index([location], map: "idx_unit_location", type: Gist)
|
@@index([location], map: "idx_unit_location", type: Gist)
|
||||||
@@index([district_id, location], map: "idx_units_location_district")
|
@@index([district_id, location], map: "idx_units_location_district")
|
||||||
@@index([location], map: "idx_units_location_gist", type: Gist)
|
@@index([location], map: "idx_units_location_gist", type: Gist)
|
||||||
@@index([location], type: Gist)
|
|
||||||
@@index([location], map: "units_location_idx1", type: Gist)
|
|
||||||
@@index([location], map: "units_location_idx2", type: Gist)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model patrol_units {
|
model patrol_units {
|
||||||
id String @id @unique @db.VarChar(100)
|
unit_id String @db.VarChar(20)
|
||||||
unit_id String @db.VarChar(20)
|
location_id String @db.Uuid
|
||||||
location_id String @db.Uuid
|
name String @db.VarChar(100)
|
||||||
name String @db.VarChar(100)
|
type String @db.VarChar(50)
|
||||||
type String @db.VarChar(50)
|
status String @db.VarChar(50)
|
||||||
status String @db.VarChar(50)
|
|
||||||
radius Float
|
radius Float
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
created_at DateTime @default(now()) @db.Timestamptz(6)
|
||||||
|
id String @id @unique @db.VarChar(100)
|
||||||
members officers[]
|
members officers[]
|
||||||
location locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
location locations @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
unit units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
unit units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
||||||
@@index([unit_id], map: "idx_patrol_units_unit_id")
|
@@index([unit_id], map: "idx_patrol_units_unit_id")
|
||||||
@@index([location_id], map: "idx_patrol_units_location_id")
|
@@index([location_id], map: "idx_patrol_units_location_id")
|
||||||
|
@ -347,10 +341,8 @@ model patrol_units {
|
||||||
}
|
}
|
||||||
|
|
||||||
model officers {
|
model officers {
|
||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
|
||||||
unit_id String @db.VarChar(20)
|
unit_id String @db.VarChar(20)
|
||||||
role_id String @db.Uuid
|
role_id String @db.Uuid
|
||||||
patrol_unit_id String @db.VarChar(100)
|
|
||||||
nrp String @unique @db.VarChar(100)
|
nrp String @unique @db.VarChar(100)
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
rank String? @db.VarChar(100)
|
rank String? @db.VarChar(100)
|
||||||
|
@ -360,16 +352,20 @@ model officers {
|
||||||
avatar String?
|
avatar String?
|
||||||
valid_until DateTime?
|
valid_until DateTime?
|
||||||
qr_code String?
|
qr_code String?
|
||||||
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
|
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
|
patrol_unit_id String @db.VarChar(100)
|
||||||
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
|
banned_reason String? @db.VarChar(255)
|
||||||
|
banned_until DateTime?
|
||||||
is_banned Boolean @default(false)
|
is_banned Boolean @default(false)
|
||||||
panic_strike Int @default(0)
|
panic_strike Int @default(0)
|
||||||
spoofing_attempts Int @default(0)
|
spoofing_attempts Int @default(0)
|
||||||
banned_reason String? @db.VarChar(255)
|
place_of_birth String?
|
||||||
banned_until DateTime?
|
date_of_birth DateTime? @db.Timestamptz(6)
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
patrol_units patrol_units @relation(fields: [patrol_unit_id], references: [id])
|
||||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
|
||||||
units units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
|
||||||
roles roles @relation(fields: [role_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
roles roles @relation(fields: [role_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
patrol_units patrol_units? @relation(fields: [patrol_unit_id], references: [id])
|
units units @relation(fields: [unit_id], references: [code_unit], onDelete: Cascade, onUpdate: NoAction)
|
||||||
panic_button_logs panic_button_logs[]
|
panic_button_logs panic_button_logs[]
|
||||||
|
|
||||||
@@index([unit_id], map: "idx_officers_unit_id")
|
@@index([unit_id], map: "idx_officers_unit_id")
|
||||||
|
@ -453,9 +449,9 @@ model panic_button_logs {
|
||||||
officer_id String? @db.Uuid
|
officer_id String? @db.Uuid
|
||||||
incident_id String @db.Uuid
|
incident_id String @db.Uuid
|
||||||
timestamp DateTime @db.Timestamptz(6)
|
timestamp DateTime @db.Timestamptz(6)
|
||||||
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
|
||||||
officers officers? @relation(fields: [officer_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
|
||||||
incidents incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
incidents incident_logs @relation(fields: [incident_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
officers officers? @relation(fields: [officer_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
users users @relation(fields: [user_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||||
|
|
||||||
@@index([user_id], map: "idx_panic_buttons_user_id")
|
@@index([user_id], map: "idx_panic_buttons_user_id")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue