semua fitur selesai
This commit is contained in:
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import 'dart:math';
|
||||
|
||||
class AuthController extends GetxController {
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
@ -20,6 +21,10 @@ class AuthController extends GetxController {
|
||||
final RxString phoneNumber = ''.obs;
|
||||
final RxString selectedRole = 'WARGA'.obs; // Default role
|
||||
final RxString alamatLengkap = ''.obs;
|
||||
final Rx<DateTime?> tanggalLahir = Rx<DateTime?>(null);
|
||||
final RxString rtRw = ''.obs;
|
||||
final RxString kelurahan = ''.obs;
|
||||
final RxString kecamatan = ''.obs;
|
||||
|
||||
// Form status
|
||||
final RxBool isLoading = false.obs;
|
||||
@ -96,7 +101,7 @@ class AuthController extends GetxController {
|
||||
|
||||
// Navigate based on role name
|
||||
if (roleName == null) {
|
||||
_navigateToWargaDashboard(); // Default to warga if role name not found
|
||||
await _checkWargaStatusAndNavigate(); // Default to warga if role name not found
|
||||
return;
|
||||
}
|
||||
|
||||
@ -105,6 +110,9 @@ class AuthController extends GetxController {
|
||||
_navigateToPetugasBumdesDashboard();
|
||||
break;
|
||||
case 'WARGA':
|
||||
// For WARGA role, check account status in warga_desa table
|
||||
await _checkWargaStatusAndNavigate();
|
||||
break;
|
||||
default:
|
||||
_navigateToWargaDashboard();
|
||||
break;
|
||||
@ -114,6 +122,64 @@ class AuthController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// Check warga status in warga_desa table and navigate accordingly
|
||||
Future<void> _checkWargaStatusAndNavigate() async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
errorMessage.value = 'Tidak dapat memperoleh data pengguna';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get user data from warga_desa table
|
||||
final userData =
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.select('status, keterangan')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (userData == null) {
|
||||
errorMessage.value = 'Data pengguna tidak ditemukan';
|
||||
return;
|
||||
}
|
||||
|
||||
final status = userData['status'] as String?;
|
||||
|
||||
switch (status?.toLowerCase()) {
|
||||
case 'active':
|
||||
// Allow login for active users
|
||||
_navigateToWargaDashboard();
|
||||
break;
|
||||
case 'suspended':
|
||||
// Show error for suspended users
|
||||
final keterangan =
|
||||
userData['keterangan'] as String? ?? 'Tidak ada keterangan';
|
||||
errorMessage.value =
|
||||
'Akun Anda dinonaktifkan oleh petugas. Keterangan: $keterangan';
|
||||
// Sign out the user
|
||||
await _authProvider.signOut();
|
||||
break;
|
||||
case 'pending':
|
||||
// Show error for pending users
|
||||
errorMessage.value =
|
||||
'Akun Anda sedang dalam proses verifikasi. Silakan tunggu hingga verifikasi selesai.';
|
||||
// Sign out the user
|
||||
await _authProvider.signOut();
|
||||
break;
|
||||
default:
|
||||
errorMessage.value = 'Status akun tidak valid';
|
||||
// Sign out the user
|
||||
await _authProvider.signOut();
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
errorMessage.value = 'Gagal memeriksa status akun: ${e.toString()}';
|
||||
// Sign out the user on error
|
||||
await _authProvider.signOut();
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToPetugasBumdesDashboard() {
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
}
|
||||
@ -188,60 +254,69 @@ class AuthController extends GetxController {
|
||||
|
||||
// Register user implementation
|
||||
Future<void> registerUser() async {
|
||||
// Validate all required fields
|
||||
if (email.value.isEmpty ||
|
||||
password.value.isEmpty ||
|
||||
nik.value.isEmpty ||
|
||||
phoneNumber.value.isEmpty ||
|
||||
alamatLengkap.value.isEmpty) {
|
||||
errorMessage.value = 'Semua field harus diisi';
|
||||
// Clear previous error messages
|
||||
errorMessage.value = '';
|
||||
|
||||
// Validate form fields
|
||||
if (!formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for email
|
||||
if (!GetUtils.isEmail(email.value.trim())) {
|
||||
errorMessage.value = 'Format email tidak valid';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for password
|
||||
if (password.value.length < 6) {
|
||||
errorMessage.value = 'Password minimal 6 karakter';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for NIK
|
||||
if (nik.value.length != 16) {
|
||||
errorMessage.value = 'NIK harus 16 digit';
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for phone number
|
||||
if (!phoneNumber.value.startsWith('08') || phoneNumber.value.length < 10) {
|
||||
errorMessage.value =
|
||||
'Nomor HP tidak valid (harus diawali 08 dan minimal 10 digit)';
|
||||
// Validate date of birth separately (since it's not a standard form field)
|
||||
if (!validateDateOfBirth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
// Create user with Supabase
|
||||
final response = await _authProvider.signUp(
|
||||
// Format tanggal lahir to string (YYYY-MM-DD)
|
||||
final formattedTanggalLahir =
|
||||
tanggalLahir.value != null
|
||||
? '${tanggalLahir.value!.year}-${tanggalLahir.value!.month.toString().padLeft(2, '0')}-${tanggalLahir.value!.day.toString().padLeft(2, '0')}'
|
||||
: '';
|
||||
|
||||
// Generate register_id with format REG-YYYY-1234567
|
||||
final currentYear = DateTime.now().year.toString();
|
||||
final randomDigits = _generateRandomDigits(7); // Generate 7 random digits
|
||||
final registerId = 'REG-$currentYear-$randomDigits';
|
||||
|
||||
// 1. Register user with Supabase Auth and add role_id to metadata
|
||||
final response = await _authProvider.client.auth.signUp(
|
||||
email: email.value.trim(),
|
||||
password: password.value,
|
||||
data: {
|
||||
'nik': nik.value.trim(),
|
||||
'phone_number': phoneNumber.value.trim(),
|
||||
'alamat_lengkap': alamatLengkap.value.trim(),
|
||||
'role': selectedRole.value,
|
||||
'role_id':
|
||||
'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae', // Fixed role_id for WARGA
|
||||
},
|
||||
);
|
||||
|
||||
// Check if registration was successful
|
||||
if (response.user != null) {
|
||||
// 2. Get the UID from the created auth user
|
||||
final userId = response.user!.id;
|
||||
|
||||
// 3. Insert user data into the warga_desa table
|
||||
await _authProvider.client.from('warga_desa').insert({
|
||||
'user_id': userId,
|
||||
'email': email.value.trim(),
|
||||
'nama_lengkap': nameController.text.trim(),
|
||||
'nik': nik.value.trim(),
|
||||
'status': 'pending',
|
||||
'tanggal_lahir': formattedTanggalLahir,
|
||||
'no_hp': phoneNumber.value.trim(),
|
||||
'rt_rw': rtRw.value.trim(),
|
||||
'kelurahan_desa': kelurahan.value.trim(),
|
||||
'kecamatan': kecamatan.value.trim(),
|
||||
'alamat': alamatLengkap.value.trim(),
|
||||
'register_id': registerId, // Add register_id to the warga_desa table
|
||||
});
|
||||
|
||||
// Registration successful
|
||||
Get.offNamed(Routes.REGISTRATION_SUCCESS);
|
||||
Get.offNamed(
|
||||
Routes.REGISTRATION_SUCCESS,
|
||||
arguments: {'register_id': registerId},
|
||||
);
|
||||
} else {
|
||||
errorMessage.value = 'Gagal mendaftar. Silakan coba lagi.';
|
||||
}
|
||||
@ -252,4 +327,155 @@ class AuthController extends GetxController {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate random digits of specified length
|
||||
String _generateRandomDigits(int length) {
|
||||
final random = Random();
|
||||
final buffer = StringBuffer();
|
||||
for (var i = 0; i < length; i++) {
|
||||
buffer.write(random.nextInt(10));
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
// Validation methods
|
||||
String? validateEmail(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Email tidak boleh kosong';
|
||||
}
|
||||
if (!GetUtils.isEmail(value)) {
|
||||
return 'Format email tidak valid';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validatePassword(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Password tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 8) {
|
||||
return 'Password minimal 8 karakter';
|
||||
}
|
||||
if (!value.contains(RegExp(r'[A-Z]'))) {
|
||||
return 'Password harus memiliki minimal 1 huruf besar';
|
||||
}
|
||||
if (!value.contains(RegExp(r'[0-9]'))) {
|
||||
return 'Password harus memiliki minimal 1 angka';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateConfirmPassword(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Konfirmasi password tidak boleh kosong';
|
||||
}
|
||||
if (value != password.value) {
|
||||
return 'Password tidak cocok';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateName(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Nama lengkap tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return 'Nama lengkap minimal 3 karakter';
|
||||
}
|
||||
if (!RegExp(r"^[a-zA-Z\s\.]+$").hasMatch(value)) {
|
||||
return 'Nama hanya boleh berisi huruf, spasi, titik, dan apostrof';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateNIK(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'NIK tidak boleh kosong';
|
||||
}
|
||||
if (value.length != 16) {
|
||||
return 'NIK harus 16 digit';
|
||||
}
|
||||
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
|
||||
return 'NIK hanya boleh berisi angka';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validatePhone(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'No HP tidak boleh kosong';
|
||||
}
|
||||
if (!value.startsWith('08')) {
|
||||
return 'Nomor HP harus diawali dengan 08';
|
||||
}
|
||||
if (value.length < 10 || value.length > 13) {
|
||||
return 'Nomor HP harus antara 10-13 digit';
|
||||
}
|
||||
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
|
||||
return 'Nomor HP hanya boleh berisi angka';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateRTRW(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'RT/RW tidak boleh kosong';
|
||||
}
|
||||
if (!RegExp(r'^\d{1,3}\/\d{1,3}$').hasMatch(value)) {
|
||||
return 'Format RT/RW tidak valid (contoh: 001/002)';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateKelurahan(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kelurahan/Desa tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return 'Kelurahan/Desa minimal 3 karakter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateKecamatan(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Kecamatan tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return 'Kecamatan minimal 3 karakter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? validateAlamat(String? value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Alamat lengkap tidak boleh kosong';
|
||||
}
|
||||
if (value.length < 5) {
|
||||
return 'Alamat terlalu pendek, minimal 5 karakter';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
bool validateDateOfBirth() {
|
||||
if (tanggalLahir.value == null) {
|
||||
errorMessage.value = 'Tanggal lahir harus diisi';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user is at least 17 years old
|
||||
final DateTime today = DateTime.now();
|
||||
final DateTime minimumAge = DateTime(
|
||||
today.year - 17,
|
||||
today.month,
|
||||
today.day,
|
||||
);
|
||||
|
||||
if (tanggalLahir.value!.isAfter(minimumAge)) {
|
||||
errorMessage.value = 'Anda harus berusia minimal 17 tahun';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ class LoginView extends GetView<AuthController> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Background gradient
|
||||
@ -72,18 +73,21 @@ class LoginView extends GetView<AuthController> {
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
// Main content with keyboard avoidance
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.03),
|
||||
_buildLoginCard(),
|
||||
_buildRegisterLink(),
|
||||
const SizedBox(height: 30),
|
||||
@ -103,12 +107,12 @@ class LoginView extends GetView<AuthController> {
|
||||
tag: 'logo',
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 220,
|
||||
height: 220,
|
||||
width: 180,
|
||||
height: 180,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
Icons.apartment_rounded,
|
||||
size: 180,
|
||||
size: 150,
|
||||
color: AppColors.primary,
|
||||
);
|
||||
},
|
||||
@ -123,7 +127,7 @@ class LoginView extends GetView<AuthController> {
|
||||
shadowColor: AppColors.shadow,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28.0),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -145,7 +149,7 @@ class LoginView extends GetView<AuthController> {
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Email field
|
||||
_buildInputLabel('Email'),
|
||||
@ -204,7 +208,7 @@ class LoginView extends GetView<AuthController> {
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
height: 50, // Slightly smaller height
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.isLoading.value ? null : controller.login,
|
||||
@ -309,6 +313,16 @@ class LoginView extends GetView<AuthController> {
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||
textInputAction:
|
||||
keyboardType == TextInputType.emailAddress
|
||||
? TextInputAction.next
|
||||
: TextInputAction.done,
|
||||
scrollPhysics: const ClampingScrollPhysics(),
|
||||
onChanged: (_) {
|
||||
if (controller.text.isNotEmpty) {
|
||||
this.controller.errorMessage.value = '';
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class RegistrationSuccessView extends StatefulWidget {
|
||||
const RegistrationSuccessView({Key? key}) : super(key: key);
|
||||
@ -15,10 +17,17 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
String? registerId;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Get the registration ID from arguments
|
||||
if (Get.arguments != null && Get.arguments is Map) {
|
||||
registerId = Get.arguments['register_id'] as String?;
|
||||
}
|
||||
|
||||
_animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
@ -215,7 +224,7 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
'Akun Anda telah berhasil terdaftar. Silakan masuk dengan email dan password yang telah Anda daftarkan.',
|
||||
'Akun Anda telah berhasil terdaftar. Silahkan tunggu petugas untuk melakukan verifikasi data diri anda.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
@ -224,6 +233,84 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
if (registerId != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Kode Registrasi:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.successLight,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: AppColors.success.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
registerId!,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.success,
|
||||
letterSpacing: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Icons.copy,
|
||||
size: 20,
|
||||
color: AppColors.success,
|
||||
),
|
||||
onPressed: () {
|
||||
// Copy to clipboard
|
||||
final data = ClipboardData(text: registerId!);
|
||||
Clipboard.setData(data);
|
||||
Get.snackbar(
|
||||
'Berhasil Disalin',
|
||||
'Kode registrasi telah disalin ke clipboard',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: AppColors.successLight,
|
||||
colorText: AppColors.success,
|
||||
margin: const EdgeInsets.all(16),
|
||||
);
|
||||
},
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
splashRadius: 20,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Text(
|
||||
'Simpan kode registrasi ini untuk memeriksa status pendaftaran Anda.',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.textSecondary,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user