kelola penyewa dan beberapa error fix

This commit is contained in:
Andreas Malvino
2025-07-09 16:01:10 +07:00
parent 0423c2fdf9
commit 47766bbdda
90 changed files with 2705 additions and 1555 deletions

View File

@ -1,8 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application <application
android:label="bumrent_app" android:label="BumRent"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/launcher_icon">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/images/logo_app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

@ -427,7 +427,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@ -484,7 +484,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 B

After

Width:  |  Height:  |  Size: 610 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 B

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 B

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 B

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 704 B

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 B

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Bumrent App</string> <string>BumRent</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
@ -13,7 +13,7 @@
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>bumrent_app</string> <string>BumRent</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>

View File

@ -19,12 +19,12 @@ class PetugasBumdesBinding extends Bindings {
print('Error removing controller: $e'); print('Error removing controller: $e');
} }
// Gunakan put untuk memastikan controller selalu tersedia dan permanent // Gunakan lazyPut untuk memastikan controller hanya diinisialisasi saat dibutuhkan
Get.put<PetugasBumdesDashboardController>( Get.lazyPut<PetugasBumdesDashboardController>(
PetugasBumdesDashboardController(), () => PetugasBumdesDashboardController(),
permanent: true, fenix: true, // Akan dibuat ulang jika dihapus
); );
print('✅ PetugasBumdesDashboardController registered successfully'); print('✅ PetugasBumdesDashboardController initialized successfully');
} }
} }

View File

@ -5,7 +5,15 @@ import '../modules/warga/controllers/warga_dashboard_controller.dart';
class WargaBinding extends Bindings { class WargaBinding extends Bindings {
@override @override
void dependencies() { void dependencies() {
Get.lazyPut<AuthProvider>(() => AuthProvider()); // Pastikan AuthProvider teregistrasi
Get.lazyPut<WargaDashboardController>(() => WargaDashboardController()); if (!Get.isRegistered<AuthProvider>()) {
Get.put(AuthProvider());
}
// Gunakan lazyPut untuk memastikan controller hanya diinisialisasi saat dibutuhkan
Get.lazyPut<WargaDashboardController>(
() => WargaDashboardController(),
fenix: true, // Akan dibuat ulang jika dihapus
);
} }
} }

View File

@ -80,8 +80,18 @@ class AuthProvider extends GetxService {
// Clear any cached user data or state // Clear any cached user data or state
// This method is called during logout to ensure all user-related data is cleared // This method is called during logout to ensure all user-related data is cleared
debugPrint('Clearing AuthProvider cached data'); debugPrint('Clearing AuthProvider cached data');
// Currently, signOut() handles most of the cleanup, but this method can be extended
// if additional cleanup is needed in the future // Explicitly clear any cached data that might be stored in the provider
// This is important to ensure no user data remains after logout or registration
try {
// Force refresh of the auth state
client.auth.refreshSession();
// Log the cleanup action
debugPrint('AuthProvider cached data cleared successfully');
} catch (e) {
debugPrint('Error clearing AuthProvider cached data: $e');
}
} }
User? get currentUser => client.auth.currentUser; User? get currentUser => client.auth.currentUser;
@ -669,6 +679,25 @@ class AuthProvider extends GetxService {
item, item,
); );
// Ensure updated_at is not null, use created_at as fallback
if (processedItem['updated_at'] == null &&
processedItem['created_at'] != null) {
debugPrint('updated_at is null, using created_at as fallback');
processedItem['updated_at'] = processedItem['created_at'];
} else if (processedItem['updated_at'] == null &&
processedItem['created_at'] == null) {
// If both are null, use current timestamp as last resort
debugPrint(
'Both updated_at and created_at are null, using current timestamp',
);
processedItem['updated_at'] = DateTime.now().toIso8601String();
}
// Debug the updated_at field
debugPrint(
'updated_at after processing: ${processedItem['updated_at']}',
);
// If aset_id is null and paket_id is not null, fetch package data // If aset_id is null and paket_id is not null, fetch package data
if (item['aset_id'] == null && item['paket_id'] != null) { if (item['aset_id'] == null && item['paket_id'] != null) {
final String paketId = item['paket_id']; final String paketId = item['paket_id'];

View File

@ -3,6 +3,7 @@ import 'package:get/get.dart';
import '../../../data/providers/auth_provider.dart'; import '../../../data/providers/auth_provider.dart';
import '../../../routes/app_routes.dart'; import '../../../routes/app_routes.dart';
import 'dart:math'; import 'dart:math';
import '../../../modules/warga/controllers/warga_dashboard_controller.dart';
class AuthController extends GetxController { class AuthController extends GetxController {
final AuthProvider _authProvider = Get.find<AuthProvider>(); final AuthProvider _authProvider = Get.find<AuthProvider>();
@ -185,7 +186,9 @@ class AuthController extends GetxController {
} }
void _navigateToWargaDashboard() { void _navigateToWargaDashboard() {
Get.offAllNamed(Routes.WARGA_DASHBOARD); // Navigate to warga dashboard with parameter to indicate it's coming from login
// This will trigger an immediate refresh of the data
Get.offAllNamed(Routes.WARGA_DASHBOARD, arguments: {'from_login': true});
} }
void forgotPassword() async { void forgotPassword() async {
@ -215,7 +218,7 @@ class AuthController extends GetxController {
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Link reset password telah dikirim ke email Anda', 'Link reset password telah dikirim ke email Anda',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green[100], backgroundColor: Colors.green[100],
colorText: Colors.green[800], colorText: Colors.green[800],
icon: const Icon(Icons.check_circle, color: Colors.green), icon: const Icon(Icons.check_circle, color: Colors.green),
@ -312,7 +315,18 @@ class AuthController extends GetxController {
'register_id': registerId, // Add register_id to the warga_desa table 'register_id': registerId, // Add register_id to the warga_desa table
}); });
// Registration successful // Reset registration fields BEFORE navigation to ensure clean state
resetRegistrationFields();
// Bersihkan data auth provider untuk memastikan tidak ada data user yang tersimpan
_authProvider.clearAuthData();
// Print debug message
print(
'Registration successful: Fields and controllers have been cleared',
);
// Registration successful - navigate to success page
Get.offNamed( Get.offNamed(
Routes.REGISTRATION_SUCCESS, Routes.REGISTRATION_SUCCESS,
arguments: {'register_id': registerId}, arguments: {'register_id': registerId},
@ -478,4 +492,99 @@ class AuthController extends GetxController {
return true; return true;
} }
// Check registration status by register_id and email
Future<Map<String, dynamic>?> checkRegistrationStatus(
String registerId,
String email,
) async {
try {
isLoading.value = true;
print('Checking registration status - ID: $registerId, Email: $email');
// Validasi input
if (registerId.isEmpty || email.isEmpty) {
print('Invalid input: registerId or email is empty');
return null;
}
// Query warga_desa table where register_id and email match
final response =
await _authProvider.client
.from('warga_desa')
.select(
'*',
) // Ensure we select all columns including 'keterangan'
.eq('register_id', registerId)
.eq('email', email)
.maybeSingle();
// Log response for debugging
print('Registration status query response: $response');
// Validasi hasil query
if (response == null) {
print('No matching registration found');
return null;
}
if (response is Map<String, dynamic> && response.isEmpty) {
print('Empty response received');
return null;
}
// Jika berhasil, kembalikan data
print('Registration found with status: ${response['status']}');
return response;
} catch (e) {
print('Error checking registration status: ${e.toString()}');
return null;
} finally {
isLoading.value = false;
}
}
// Reset all registration fields
void resetRegistrationFields() {
// Reset text controllers
emailController.clear();
passwordController.clear();
nameController.clear();
confirmPasswordController.clear();
// Reset form fields
email.value = '';
password.value = '';
nik.value = '';
phoneNumber.value = '';
selectedRole.value = 'WARGA'; // Reset to default role
alamatLengkap.value = '';
tanggalLahir.value = null;
rtRw.value = '';
kelurahan.value = '';
kecamatan.value = '';
// Reset form status
isPasswordVisible.value = false;
isConfirmPasswordVisible.value = false;
errorMessage.value = '';
// Reset form key if needed
if (formKey.currentState != null) {
formKey.currentState!.reset();
}
// Bersihkan WargaDashboardController jika terdaftar
try {
if (Get.isRegistered<WargaDashboardController>()) {
print(
'Removing WargaDashboardController to ensure clean state after registration',
);
Get.delete<WargaDashboardController>(force: true);
}
} catch (e) {
print('Error removing WargaDashboardController: $e');
}
}
} }

View File

@ -9,7 +9,7 @@ class LoginView extends GetView<AuthController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: true, resizeToAvoidBottomInset: false,
body: Stack( body: Stack(
children: [ children: [
// Background gradient // Background gradient
@ -77,9 +77,6 @@ class LoginView extends GetView<AuthController> {
SafeArea( SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0), padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column( child: Column(

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../theme/app_colors.dart'; import '../../../theme/app_colors.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import '../../../routes/app_routes.dart';
class RegistrationSuccessView extends StatefulWidget { class RegistrationSuccessView extends StatefulWidget {
const RegistrationSuccessView({Key? key}) : super(key: key); const RegistrationSuccessView({Key? key}) : super(key: key);
@ -284,7 +285,7 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
Get.snackbar( Get.snackbar(
'Berhasil Disalin', 'Berhasil Disalin',
'Kode registrasi telah disalin ke clipboard', 'Kode registrasi telah disalin ke clipboard',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: AppColors.successLight, backgroundColor: AppColors.successLight,
colorText: AppColors.success, colorText: AppColors.success,
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
@ -329,7 +330,7 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () {
// Navigate back to login page // Navigate back to login page
Get.offAllNamed('/login'); Get.offNamed(Routes.LOGIN);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary, backgroundColor: AppColors.primary,

View File

@ -2,100 +2,108 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../controllers/auth_controller.dart'; import '../controllers/auth_controller.dart';
import '../../../theme/app_colors.dart'; import '../../../theme/app_colors.dart';
import 'package:intl/intl.dart';
class RegistrationView extends GetView<AuthController> { class RegistrationView extends GetView<AuthController> {
const RegistrationView({Key? key}) : super(key: key); const RegistrationView({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return WillPopScope(
body: Stack( onWillPop: () async {
children: [ // Reset all registration fields when leaving the page
// Background gradient - same as login page controller.resetRegistrationFields();
Container( return true;
decoration: BoxDecoration( },
gradient: LinearGradient( child: Scaffold(
begin: Alignment.topRight, body: Stack(
end: Alignment.bottomLeft, children: [
colors: [ // Background gradient - same as login page
AppColors.primaryLight.withOpacity(0.1), Container(
AppColors.background,
AppColors.accentLight.withOpacity(0.1),
],
),
),
),
// Pattern overlay - same as login page
Opacity(
opacity: 0.03,
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.blue[50], // Temporary solid color gradient: LinearGradient(
), begin: Alignment.topRight,
), end: Alignment.bottomLeft,
),
// Accent circles - same as login page
Positioned(
top: -40,
right: -20,
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [ colors: [
AppColors.primary.withOpacity(0.2), AppColors.primaryLight.withOpacity(0.1),
Colors.transparent, AppColors.background,
AppColors.accentLight.withOpacity(0.1),
], ],
), ),
), ),
), ),
),
Positioned(
bottom: -50,
left: -30,
child: Container(
width: 180,
height: 180,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
AppColors.accent.withOpacity(0.2),
Colors.transparent,
],
),
),
),
),
// Main content // Pattern overlay - same as login page
SafeArea( Opacity(
child: SingleChildScrollView( opacity: 0.03,
physics: const BouncingScrollPhysics(), child: Container(
child: Padding( decoration: BoxDecoration(
padding: const EdgeInsets.symmetric(horizontal: 24.0), color: Colors.blue[50], // Temporary solid color
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
_buildBackButton(),
const SizedBox(height: 20),
_buildHeader(),
const SizedBox(height: 30),
_buildRegistrationCard(),
_buildLoginLink(),
_buildCheckStatusLink(),
const SizedBox(height: 30),
],
), ),
), ),
), ),
),
], // Accent circles - same as login page
Positioned(
top: -40,
right: -20,
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
AppColors.primary.withOpacity(0.2),
Colors.transparent,
],
),
),
),
),
Positioned(
bottom: -50,
left: -30,
child: Container(
width: 180,
height: 180,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
AppColors.accent.withOpacity(0.2),
Colors.transparent,
],
),
),
),
),
// Main content
SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 20),
_buildBackButton(),
const SizedBox(height: 20),
_buildHeader(),
const SizedBox(height: 30),
_buildRegistrationCard(),
_buildLoginLink(),
_buildCheckStatusLink(),
const SizedBox(height: 30),
],
),
),
),
),
],
),
), ),
); );
} }
@ -104,7 +112,11 @@ class RegistrationView extends GetView<AuthController> {
return Align( return Align(
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: InkWell( child: InkWell(
onTap: () => Get.back(), onTap: () {
// Reset all registration fields when going back
controller.resetRegistrationFields();
Get.back();
},
borderRadius: BorderRadius.circular(50), borderRadius: BorderRadius.circular(50),
child: Container( child: Container(
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
@ -644,7 +656,11 @@ class RegistrationView extends GetView<AuthController> {
style: TextStyle(color: AppColors.textSecondary), style: TextStyle(color: AppColors.textSecondary),
), ),
TextButton( TextButton(
onPressed: () => Get.back(), onPressed: () {
// Reset all registration fields when going back to login
controller.resetRegistrationFields();
Get.back();
},
style: TextButton.styleFrom( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
), ),
@ -694,7 +710,8 @@ class RegistrationView extends GetView<AuthController> {
// Show dialog to check registration status // Show dialog to check registration status
void _showCheckStatusDialog(BuildContext context) { void _showCheckStatusDialog(BuildContext context) {
final TextEditingController codeController = TextEditingController(); final TextEditingController codeController = TextEditingController();
final TextEditingController identifierController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final authController = Get.find<AuthController>();
showDialog( showDialog(
context: context, context: context,
@ -788,9 +805,9 @@ class RegistrationView extends GetView<AuthController> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Email/NIK/Phone field // Email field
Text( Text(
'Email/NIK/No HP', 'Email',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.textPrimary, color: AppColors.textPrimary,
@ -799,12 +816,13 @@ class RegistrationView extends GetView<AuthController> {
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
TextField( TextField(
controller: identifierController, controller: emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Masukkan email, NIK, atau no HP', hintText: 'Masukkan email yang terdaftar',
hintStyle: TextStyle(color: AppColors.textLight), hintStyle: TextStyle(color: AppColors.textLight),
prefixIcon: Icon( prefixIcon: Icon(
Icons.person_outline, Icons.email_outlined,
color: AppColors.iconGrey, color: AppColors.iconGrey,
size: 22, size: 22,
), ),
@ -835,20 +853,99 @@ class RegistrationView extends GetView<AuthController> {
width: double.infinity, width: double.infinity,
height: 48, height: 48,
child: ElevatedButton( child: ElevatedButton(
onPressed: () { onPressed: () async {
// TODO: Implement check status functionality // Validasi input
if (codeController.text.isEmpty ||
emailController.text.isEmpty) {
Get.snackbar(
'Error',
'Kode registrasi dan Email harus diisi',
snackPosition: SnackPosition.TOP,
backgroundColor: AppColors.errorLight,
colorText: AppColors.error,
icon: Icon(
Icons.error_outline,
color: AppColors.error,
),
duration: const Duration(seconds: 3),
);
return;
}
// Simpan nilai input untuk digunakan nanti
final String registerId = codeController.text.trim();
final String email = emailController.text.trim();
print(
'Checking registration status for ID: $registerId, Email: $email',
);
// Tutup dialog input
Navigator.pop(context); Navigator.pop(context);
// Show a mock response for now // Tampilkan loading indicator
Get.snackbar( Get.dialog(
'Status Pendaftaran', const Center(child: CircularProgressIndicator()),
'Status pendaftaran sedang diproses. Silakan tunggu konfirmasi lebih lanjut.', barrierDismissible: false,
snackPosition: SnackPosition.BOTTOM,
backgroundColor: AppColors.infoLight,
colorText: AppColors.info,
icon: Icon(Icons.info_outline, color: AppColors.info),
duration: const Duration(seconds: 4),
); );
try {
// Cek status pendaftaran
final result = await authController
.checkRegistrationStatus(registerId, email);
// Debug log
print('Registration status check result: $result');
// Tutup dialog loading
Get.back();
// Periksa hasil query
if (result != null && result.isNotEmpty) {
print('Valid result found, showing status dialog');
// Tambahkan delay kecil untuk memastikan UI diperbarui dengan benar
// Ini membantu memastikan dialog status muncul setelah dialog loading ditutup
Future.delayed(const Duration(milliseconds: 100), () {
_showRegistrationStatusDialog(result);
});
} else {
print('No result found or empty result');
// Tampilkan pesan error
Get.snackbar(
'Tidak Ditemukan',
'Data pendaftaran tidak ditemukan. Pastikan kode registrasi dan email yang dimasukkan benar.',
snackPosition: SnackPosition.TOP,
backgroundColor: AppColors.errorLight,
colorText: AppColors.error,
icon: Icon(
Icons.error_outline,
color: AppColors.error,
),
duration: const Duration(seconds: 4),
);
}
} catch (e) {
// Tutup dialog loading jika terjadi error
Get.back();
print('Error checking registration status: $e');
// Tampilkan pesan error
Get.snackbar(
'Error',
'Terjadi kesalahan saat memeriksa status. Silakan coba lagi nanti.',
snackPosition: SnackPosition.TOP,
backgroundColor: AppColors.errorLight,
colorText: AppColors.error,
icon: Icon(
Icons.error_outline,
color: AppColors.error,
),
duration: const Duration(seconds: 4),
);
}
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accent, backgroundColor: AppColors.accent,
@ -875,6 +972,219 @@ class RegistrationView extends GetView<AuthController> {
); );
} }
// Show dialog with registration status details
void _showRegistrationStatusDialog(Map<String, dynamic> data) {
// Log data yang diterima untuk debugging
print('Showing registration status dialog with data: $data');
// Validasi data
if (data.isEmpty) {
print('Error: Empty data received in _showRegistrationStatusDialog');
Get.snackbar(
'Error',
'Data status pendaftaran tidak valid',
snackPosition: SnackPosition.TOP,
backgroundColor: AppColors.errorLight,
colorText: AppColors.error,
duration: const Duration(seconds: 3),
);
return;
}
final String status = data['status'] ?? 'unknown';
final String namaLengkap = data['nama_lengkap'] ?? '';
final String keterangan = data['keterangan'] ?? '';
print('Processing status: $status, nama: $namaLengkap');
// Define status color and message based on status
Color statusColor;
String statusMessage;
IconData statusIcon;
switch (status) {
case 'pending':
statusColor = AppColors.warning;
statusMessage =
keterangan.isNotEmpty
? keterangan
: 'Pendaftaran Anda sedang dalam proses verifikasi oleh petugas. Mohon tunggu konfirmasi lebih lanjut.';
statusIcon = Icons.hourglass_top;
break;
case 'active':
statusColor = AppColors.success;
statusMessage =
keterangan.isNotEmpty
? keterangan
: 'Pendaftaran Anda telah disetujui. Anda dapat login menggunakan email dan password yang telah didaftarkan.';
statusIcon = Icons.check_circle;
break;
case 'dibatalkan':
statusColor = AppColors.error;
statusMessage =
keterangan.isNotEmpty
? keterangan
: 'Maaf, pendaftaran Anda ditolak. Silakan hubungi admin untuk informasi lebih lanjut.';
statusIcon = Icons.cancel;
break;
case 'suspended':
statusColor = AppColors.error;
statusMessage =
keterangan.isNotEmpty
? keterangan
: 'Akun Anda saat ini dinonaktifkan. Silakan hubungi admin untuk informasi lebih lanjut.';
statusIcon = Icons.block;
break;
default:
statusColor = AppColors.textSecondary;
statusMessage =
keterangan.isNotEmpty
? keterangan
: 'Status pendaftaran tidak diketahui. Silakan hubungi admin untuk informasi lebih lanjut.';
statusIcon = Icons.help;
}
print('Preparing to show dialog with status: $status, color: $statusColor');
// Gunakan Get.dialog sebagai pengganti showDialog untuk menghindari masalah context
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(statusIcon, color: statusColor, size: 40),
),
const SizedBox(height: 24),
Text(
'Status Pendaftaran',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 8),
Text(
namaLengkap,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(30),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
),
const SizedBox(height: 24),
Text(
statusMessage,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,
color: AppColors.textSecondary,
height: 1.5,
),
),
// Tampilkan informasi tambahan jika ada
if (data.containsKey('tanggal_update') &&
data['tanggal_update'] != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.infoLight.withOpacity(0.3),
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
size: 16,
color: AppColors.info,
),
const SizedBox(width: 8),
Text(
'Informasi Tambahan',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: AppColors.info,
),
),
],
),
const SizedBox(height: 8),
Text(
'Terakhir diperbarui: ${_formatDate(data['tanggal_update'])}',
style: TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
),
),
],
),
),
],
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Get.back(),
style: ElevatedButton.styleFrom(
backgroundColor: statusColor,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14),
),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text(
'Tutup',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
],
),
),
),
barrierDismissible: true,
);
print('Dialog setup completed');
}
// New method to build date picker field // New method to build date picker field
Widget _buildDateField(BuildContext context) { Widget _buildDateField(BuildContext context) {
return GestureDetector( return GestureDetector(
@ -944,4 +1254,24 @@ class RegistrationView extends GetView<AuthController> {
}), }),
); );
} }
String _formatDate(dynamic date) {
if (date is String) {
try {
final DateTime parsedDate = DateTime.parse(date);
return DateFormat('dd MMM yyyy, HH:mm').format(parsedDate);
} catch (e) {
return date;
}
} else if (date is num) {
final DateTime parsedDate = DateTime.fromMillisecondsSinceEpoch(
date.toInt() * 1000,
);
return DateFormat('dd MMM yyyy, HH:mm').format(parsedDate);
} else if (date is DateTime) {
return DateFormat('dd MMM yyyy, HH:mm').format(date);
} else {
return "Format tanggal tidak diketahui";
}
}
} }

View File

@ -49,7 +49,7 @@ class ListPetugasMitraController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Petugas mitra berhasil ditambahkan', 'Petugas mitra berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -62,7 +62,7 @@ class ListPetugasMitraController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Data petugas mitra berhasil diperbarui', 'Data petugas mitra berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -73,7 +73,7 @@ class ListPetugasMitraController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Petugas mitra berhasil dihapus', 'Petugas mitra berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -86,7 +86,7 @@ class ListPetugasMitraController extends GetxController {
Get.snackbar( Get.snackbar(
'Status Diperbarui', 'Status Diperbarui',
'Status petugas mitra diubah menjadi ${!currentStatus ? 'Aktif' : 'Nonaktif'}', 'Status petugas mitra diubah menjadi ${!currentStatus ? 'Aktif' : 'Nonaktif'}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }

View File

@ -122,7 +122,7 @@ class PetugasAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Error Memuat Data', 'Error Memuat Data',
'Gagal mengambil data aset dari server. Silakan coba lagi nanti.', 'Gagal mengambil data aset dari server. Silakan coba lagi nanti.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -258,7 +258,7 @@ class PetugasAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Terjadi kesalahan saat menghapus aset', 'Terjadi kesalahan saat menghapus aset',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -274,7 +274,7 @@ class PetugasAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal menghapus aset: ${e.toString()}', 'Gagal menghapus aset: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -66,7 +66,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memuat data. Silakan coba lagi.', 'Gagal memuat data. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -92,7 +92,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Rekening Utama', 'Rekening Utama',
'Rekening ${account['bank_name']} telah dijadikan rekening utama', 'Rekening ${account['bank_name']} telah dijadikan rekening utama',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -109,7 +109,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Rekening Ditambahkan', 'Rekening Ditambahkan',
'Rekening bank baru telah berhasil ditambahkan', 'Rekening bank baru telah berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -125,7 +125,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Rekening Diperbarui', 'Rekening Diperbarui',
'Informasi rekening bank telah berhasil diperbarui', 'Informasi rekening bank telah berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -138,7 +138,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Tidak Dapat Menghapus', 'Tidak Dapat Menghapus',
'Rekening utama tidak dapat dihapus. Silakan atur rekening lain sebagai utama terlebih dahulu.', 'Rekening utama tidak dapat dihapus. Silakan atur rekening lain sebagai utama terlebih dahulu.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
return; return;
} }
@ -148,7 +148,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Rekening Dihapus', 'Rekening Dihapus',
'Rekening bank telah berhasil dihapus', 'Rekening bank telah berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -164,7 +164,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Status Diperbarui', 'Status Diperbarui',
'Status mitra telah diubah menjadi ${partner['is_active'] ? 'Aktif' : 'Tidak Aktif'}', 'Status mitra telah diubah menjadi ${partner['is_active'] ? 'Aktif' : 'Tidak Aktif'}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -181,7 +181,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Mitra Ditambahkan', 'Mitra Ditambahkan',
'Mitra baru telah berhasil ditambahkan', 'Mitra baru telah berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -197,7 +197,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Mitra Diperbarui', 'Mitra Diperbarui',
'Informasi mitra telah berhasil diperbarui', 'Informasi mitra telah berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -210,7 +210,7 @@ class PetugasBumdesCbpController extends GetxController {
Get.snackbar( Get.snackbar(
'Mitra Dihapus', 'Mitra Dihapus',
'Mitra telah berhasil dihapus', 'Mitra telah berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }

View File

@ -1,12 +1,14 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../data/providers/auth_provider.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import '../../../routes/app_routes.dart'; import 'package:bumrent_app/app/data/providers/auth_provider.dart';
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
import 'package:bumrent_app/app/data/providers/pesanan_provider.dart';
import 'package:bumrent_app/app/routes/app_routes.dart';
import '../../../services/sewa_service.dart'; import '../../../services/sewa_service.dart';
import '../../../services/service_manager.dart'; import '../../../services/service_manager.dart';
import '../../../data/models/pembayaran_model.dart'; import '../../../data/models/pembayaran_model.dart';
import '../../../services/pembayaran_service.dart'; import '../../../services/pembayaran_service.dart';
import '../../../data/providers/aset_provider.dart';
import '../../../data/providers/pesanan_provider.dart';
class PetugasBumdesDashboardController extends GetxController { class PetugasBumdesDashboardController extends GetxController {
AuthProvider? _authProvider; AuthProvider? _authProvider;
@ -297,37 +299,56 @@ class PetugasBumdesDashboardController extends GetxController {
void logout() async { void logout() async {
try { try {
// Clear providers data // Store login route for navigation
final loginRoute = Routes.LOGIN;
// Sign out from Supabase
if (_authProvider != null) { if (_authProvider != null) {
// Sign out from Supabase
await _authProvider!.signOut(); await _authProvider!.signOut();
// Clear auth provider data
_authProvider!.clearAuthData();
// Clear aset provider data
try {
final asetProvider = Get.find<AsetProvider>();
asetProvider.clearCache();
} catch (e) {
print('Error clearing AsetProvider: $e');
}
// Clear pesanan provider data
try {
final pesananProvider = Get.find<PesananProvider>();
pesananProvider.clearCache();
} catch (e) {
print('Error clearing PesananProvider: $e');
}
} }
// Navigate to login screen // Navigate to login screen while context is still valid
Get.offAllNamed(Routes.LOGIN); Get.offAllNamed(loginRoute);
// Clear auth provider data if available
if (_authProvider != null) {
_authProvider!.clearAuthData();
}
// Clear provider caches
_clearProviderCaches();
// Clean up GetX controllers but keep navigation intact
Get.deleteAll(force: false);
} catch (e) { } catch (e) {
print('Error during logout: $e'); print('Error during logout: $e');
// Still try to navigate to login even if sign out fails // Still try to navigate to login even if sign out fails
Get.offAllNamed(Routes.LOGIN); Get.offAllNamed(Routes.LOGIN);
} }
} }
// Helper method to clear provider caches that need explicit clearing
void _clearProviderCaches() {
try {
// Clear AsetProvider cache
if (Get.isRegistered<AsetProvider>()) {
final asetProvider = Get.find<AsetProvider>();
asetProvider.clearCache();
}
} catch (e) {
print('Error clearing AsetProvider: $e');
}
try {
// Clear PesananProvider cache
if (Get.isRegistered<PesananProvider>()) {
final pesananProvider = Get.find<PesananProvider>();
pesananProvider.clearCache();
}
} catch (e) {
print('Error clearing PesananProvider: $e');
}
// Add other providers here that need explicit cache clearing
}
} }

View File

@ -21,7 +21,7 @@ class PetugasDetailPenyewaController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Data penyewa tidak ditemukan', 'Data penyewa tidak ditemukan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -46,7 +46,7 @@ class PetugasDetailPenyewaController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memuat data penyewa', 'Gagal memuat data penyewa',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -203,14 +203,14 @@ class PetugasDetailPenyewaController extends GetxController {
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Status penyewa berhasil diperbarui', 'Status penyewa berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} catch (e) { } catch (e) {
print('Error updating penyewa status: $e'); print('Error updating penyewa status: $e');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memperbarui status penyewa', 'Gagal memperbarui status penyewa',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isLoading.value = false; isLoading.value = false;

View File

@ -184,7 +184,7 @@ class PetugasLaporanController extends GetxController {
'Gagal membuat laporan: $e', 'Gagal membuat laporan: $e',
backgroundColor: Colors.red[100], backgroundColor: Colors.red[100],
colorText: Colors.red[900], colorText: Colors.red[900],
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isLoading.value = false; isLoading.value = false;
@ -1641,7 +1641,7 @@ class PetugasLaporanController extends GetxController {
'Harap generate laporan terlebih dahulu', 'Harap generate laporan terlebih dahulu',
backgroundColor: Colors.amber[100], backgroundColor: Colors.amber[100],
colorText: Colors.amber[900], colorText: Colors.amber[900],
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -1654,7 +1654,7 @@ class PetugasLaporanController extends GetxController {
'Harap generate laporan terlebih dahulu', 'Harap generate laporan terlebih dahulu',
backgroundColor: Colors.amber[100], backgroundColor: Colors.amber[100],
colorText: Colors.amber[900], colorText: Colors.amber[900],
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
return; return;
} }
@ -1687,7 +1687,7 @@ class PetugasLaporanController extends GetxController {
'Laporan disimpan di ${file.path}', 'Laporan disimpan di ${file.path}',
backgroundColor: Colors.green[100], backgroundColor: Colors.green[100],
colorText: Colors.green[900], colorText: Colors.green[900],
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
duration: const Duration(seconds: 5), duration: const Duration(seconds: 5),
); );
@ -1701,7 +1701,7 @@ class PetugasLaporanController extends GetxController {
'Gagal menyimpan laporan: $e', 'Gagal menyimpan laporan: $e',
backgroundColor: Colors.red[100], backgroundColor: Colors.red[100],
colorText: Colors.red[900], colorText: Colors.red[900],
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }
@ -1714,7 +1714,7 @@ class PetugasLaporanController extends GetxController {
'Harap generate laporan terlebih dahulu', 'Harap generate laporan terlebih dahulu',
backgroundColor: Colors.amber[100], backgroundColor: Colors.amber[100],
colorText: Colors.amber[900], colorText: Colors.amber[900],
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
return; return;
} }

View File

@ -79,7 +79,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Rekening utama berhasil diubah', 'Rekening utama berhasil diubah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -93,7 +93,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Status mitra berhasil diubah', 'Status mitra berhasil diubah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -110,7 +110,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Rekening bank berhasil ditambahkan', 'Rekening bank berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -124,7 +124,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Rekening bank berhasil diperbarui', 'Rekening bank berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -145,7 +145,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Rekening bank berhasil dihapus', 'Rekening bank berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -155,7 +155,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Mitra berhasil ditambahkan', 'Mitra berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -166,7 +166,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Mitra berhasil diperbarui', 'Mitra berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -177,7 +177,7 @@ class PetugasManajemenBumdesController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Mitra berhasil dihapus', 'Mitra berhasil dihapus',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }

View File

@ -87,7 +87,7 @@ class PetugasPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memuat data paket. Silakan coba lagi.', 'Gagal memuat data paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -320,7 +320,7 @@ class PetugasPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Paket baru berhasil ditambahkan', 'Paket baru berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -334,7 +334,7 @@ class PetugasPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal menambahkan paket. Silakan coba lagi.', 'Gagal menambahkan paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -373,7 +373,7 @@ class PetugasPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Paket berhasil diperbarui', 'Paket berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -388,7 +388,7 @@ class PetugasPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memperbarui paket. Silakan coba lagi.', 'Gagal memperbarui paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -440,7 +440,7 @@ class PetugasPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Terjadi kesalahan saat menghapus paket', 'Terjadi kesalahan saat menghapus paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
@ -462,7 +462,7 @@ class PetugasPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal menghapus paket: ${e.toString()}', 'Gagal menghapus paket: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),

View File

@ -6,6 +6,7 @@ class PetugasPenyewaController extends GetxController {
// Reactive variables // Reactive variables
final isLoading = true.obs; final isLoading = true.obs;
final isRefreshing = false.obs;
final penyewaList = <Map<String, dynamic>>[].obs; final penyewaList = <Map<String, dynamic>>[].obs;
final filteredPenyewaList = <Map<String, dynamic>>[].obs; final filteredPenyewaList = <Map<String, dynamic>>[].obs;
final filterStatus = 'all'.obs; final filterStatus = 'all'.obs;
@ -30,6 +31,39 @@ class PetugasPenyewaController extends GetxController {
fetchPenyewaList(); fetchPenyewaList();
} }
// Method khusus untuk pull-to-refresh yang tidak menampilkan loading spinner penuh layar
Future<void> refreshPenyewaList() async {
try {
isRefreshing.value = true;
// Get all penyewa data without filtering
final data =
await _authProvider.client
.from('warga_desa')
.select(
'user_id, nama_lengkap, email, nik, no_hp, avatar, status, keterangan',
)
as List<dynamic>;
// Filter out rows where user_id is null
final filteredData = data.where((row) => row['user_id'] != null).toList();
// Get total sewa count for each user
final enrichedData = await _enrichWithSewaCount(filteredData);
penyewaList.value = enrichedData;
// Apply filters to update filteredPenyewaList
applyFilters();
// Tampilkan pesan sukses
} catch (e) {
print('Error refreshing penyewa list: $e');
} finally {
isRefreshing.value = false;
}
}
void changeTab(int index) { void changeTab(int index) {
currentTabIndex.value = index; currentTabIndex.value = index;
applyFilters(); applyFilters();
@ -180,14 +214,14 @@ class PetugasPenyewaController extends GetxController {
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Status penyewa berhasil diperbarui', 'Status penyewa berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} catch (e) { } catch (e) {
print('Error updating penyewa status: $e'); print('Error updating penyewa status: $e');
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Terjadi kesalahan saat memperbarui status penyewa', 'Terjadi kesalahan saat memperbarui status penyewa',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isLoading.value = false; isLoading.value = false;

View File

@ -4,6 +4,7 @@ import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:bumrent_app/app/data/models/aset_model.dart'; import 'package:bumrent_app/app/data/models/aset_model.dart';
import 'package:bumrent_app/app/data/providers/aset_provider.dart'; import 'package:bumrent_app/app/data/providers/aset_provider.dart';
import 'package:bumrent_app/app/routes/app_routes.dart';
class PetugasTambahAsetController extends GetxController { class PetugasTambahAsetController extends GetxController {
// Flag to check if in edit mode // Flag to check if in edit mode
@ -13,34 +14,38 @@ class PetugasTambahAsetController extends GetxController {
@override @override
Future<void> onInit() async { Future<void> onInit() async {
super.onInit(); super.onInit();
try { try {
// Handle edit mode and load data if needed // Handle edit mode and load data if needed
final args = Get.arguments; final args = Get.arguments;
debugPrint('[DEBUG] PetugasTambahAsetController initialized with args: $args'); debugPrint(
'[DEBUG] PetugasTambahAsetController initialized with args: $args',
);
if (args != null && args is Map<String, dynamic>) { if (args != null && args is Map<String, dynamic>) {
isEditing.value = args['isEditing'] ?? false; isEditing.value = args['isEditing'] ?? false;
debugPrint('[DEBUG] isEditing set to: ${isEditing.value}'); debugPrint('[DEBUG] isEditing set to: ${isEditing.value}');
if (isEditing.value) { if (isEditing.value) {
// Get asset ID from arguments // Get asset ID from arguments
final assetId = args['assetId']?.toString() ?? ''; final assetId = args['assetId']?.toString() ?? '';
debugPrint('[DEBUG] Edit mode: Loading asset with ID: $assetId'); debugPrint('[DEBUG] Edit mode: Loading asset with ID: $assetId');
if (assetId.isNotEmpty) { if (assetId.isNotEmpty) {
// Store the asset ID and load asset data // Store the asset ID and load asset data
this.assetId = assetId; this.assetId = assetId;
debugPrint('[DEBUG] Asset ID set to: $assetId'); debugPrint('[DEBUG] Asset ID set to: $assetId');
// Load asset data and await completion // Load asset data and await completion
await _loadAssetData(assetId); await _loadAssetData(assetId);
} else { } else {
debugPrint('[ERROR] Edit mode but no assetId provided in arguments'); debugPrint(
'[ERROR] Edit mode but no assetId provided in arguments',
);
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID Aset tidak ditemukan', 'ID Aset tidak ditemukan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
// Optionally navigate back if in edit mode without an ID // Optionally navigate back if in edit mode without an ID
Future.delayed(Duration.zero, () => Get.back()); Future.delayed(Duration.zero, () => Get.back());
@ -53,7 +58,9 @@ class PetugasTambahAsetController extends GetxController {
} }
} else { } else {
// Default values for new asset when no arguments are passed // Default values for new asset when no arguments are passed
debugPrint('[DEBUG] No arguments passed, defaulting to add new asset mode'); debugPrint(
'[DEBUG] No arguments passed, defaulting to add new asset mode',
);
quantityController.text = '1'; quantityController.text = '1';
unitOfMeasureController.text = 'Unit'; unitOfMeasureController.text = 'Unit';
} }
@ -62,11 +69,11 @@ class PetugasTambahAsetController extends GetxController {
debugPrint('Stack trace: $stackTrace'); debugPrint('Stack trace: $stackTrace');
// Ensure loading is set to false even if there's an error // Ensure loading is set to false even if there's an error
isLoading.value = false; isLoading.value = false;
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memuat data', 'Terjadi kesalahan saat memuat data',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
@ -85,21 +92,21 @@ class PetugasTambahAsetController extends GetxController {
try { try {
isLoading.value = true; isLoading.value = true;
debugPrint('[DEBUG] Fetching asset data for ID: $assetId'); debugPrint('[DEBUG] Fetching asset data for ID: $assetId');
// Fetch asset data from Supabase // Fetch asset data from Supabase
final aset = await _asetProvider.getAsetById(assetId); final aset = await _asetProvider.getAsetById(assetId);
if (aset == null) { if (aset == null) {
throw Exception('Aset tidak ditemukan'); throw Exception('Aset tidak ditemukan');
} }
debugPrint('[DEBUG] Successfully fetched asset data: ${aset.toJson()}'); debugPrint('[DEBUG] Successfully fetched asset data: ${aset.toJson()}');
// Populate form fields with the fetched data // Populate form fields with the fetched data
nameController.text = aset.nama ?? ''; nameController.text = aset.nama ?? '';
descriptionController.text = aset.deskripsi ?? ''; descriptionController.text = aset.deskripsi ?? '';
quantityController.text = (aset.kuantitas ?? 1).toString(); quantityController.text = (aset.kuantitas ?? 1).toString();
// Ensure the status matches one of the available options exactly // Ensure the status matches one of the available options exactly
final status = aset.status?.toLowerCase() ?? 'tersedia'; final status = aset.status?.toLowerCase() ?? 'tersedia';
if (status == 'tersedia') { if (status == 'tersedia') {
@ -110,18 +117,19 @@ class PetugasTambahAsetController extends GetxController {
// Default to 'Tersedia' if status is not recognized // Default to 'Tersedia' if status is not recognized
selectedStatus.value = 'Tersedia'; selectedStatus.value = 'Tersedia';
} }
// Handle time options and pricing // Handle time options and pricing
if (aset.satuanWaktuSewa != null && aset.satuanWaktuSewa!.isNotEmpty) { if (aset.satuanWaktuSewa != null && aset.satuanWaktuSewa!.isNotEmpty) {
// Reset time options // Reset time options
timeOptions.forEach((key, value) => value.value = false); timeOptions.forEach((key, value) => value.value = false);
// Process each satuan waktu sewa // Process each satuan waktu sewa
for (var sws in aset.satuanWaktuSewa) { for (var sws in aset.satuanWaktuSewa) {
final satuan = sws['nama_satuan_waktu']?.toString().toLowerCase() ?? ''; final satuan =
sws['nama_satuan_waktu']?.toString().toLowerCase() ?? '';
final harga = sws['harga'] as int? ?? 0; final harga = sws['harga'] as int? ?? 0;
final maksimalWaktu = sws['maksimal_waktu'] as int? ?? 24; final maksimalWaktu = sws['maksimal_waktu'] as int? ?? 24;
if (satuan.contains('jam')) { if (satuan.contains('jam')) {
timeOptions['Per Jam']?.value = true; timeOptions['Per Jam']?.value = true;
pricePerHourController.text = harga.toString(); pricePerHourController.text = harga.toString();
@ -133,19 +141,21 @@ class PetugasTambahAsetController extends GetxController {
} }
} }
} }
// Clear existing images // Clear existing images
selectedImages.clear(); selectedImages.clear();
networkImageUrls.clear(); networkImageUrls.clear();
// Get all image URLs from the model // Get all image URLs from the model
final allImageUrls = aset.imageUrls.toList(); final allImageUrls = aset.imageUrls.toList();
// If no imageUrls but has imageUrl, use that as fallback (backward compatibility) // If no imageUrls but has imageUrl, use that as fallback (backward compatibility)
if (allImageUrls.isEmpty && aset.imageUrl != null && aset.imageUrl!.isNotEmpty) { if (allImageUrls.isEmpty &&
aset.imageUrl != null &&
aset.imageUrl!.isNotEmpty) {
allImageUrls.add(aset.imageUrl!); allImageUrls.add(aset.imageUrl!);
} }
// Add all images to the lists // Add all images to the lists
for (final imageUrl in allImageUrls) { for (final imageUrl in allImageUrls) {
if (imageUrl != null && imageUrl.isNotEmpty) { if (imageUrl != null && imageUrl.isNotEmpty) {
@ -161,27 +171,30 @@ class PetugasTambahAsetController extends GetxController {
} }
} }
} }
debugPrint('Total ${networkImageUrls.length} images loaded for asset $assetId'); debugPrint(
'Total ${networkImageUrls.length} images loaded for asset $assetId',
);
debugPrint('[DEBUG] Successfully loaded asset data for ID: $assetId'); debugPrint('[DEBUG] Successfully loaded asset data for ID: $assetId');
} catch (e, stackTrace) { } catch (e, stackTrace) {
debugPrint('[ERROR] Failed to load asset data: $e'); debugPrint('[ERROR] Failed to load asset data: $e');
debugPrint('Stack trace: $stackTrace'); debugPrint('Stack trace: $stackTrace');
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memuat data aset: ${e.toString()}', 'Gagal memuat data aset: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
// Optionally navigate back if there's an error // Optionally navigate back if there's an error
Future.delayed(const Duration(seconds: 2), () => Get.back()); Future.delayed(const Duration(seconds: 2), () => Get.back());
} finally { } finally {
isLoading.value = false; isLoading.value = false;
} }
} }
// Form controllers // Form controllers
final nameController = TextEditingController(); final nameController = TextEditingController();
final descriptionController = TextEditingController(); final descriptionController = TextEditingController();
@ -213,8 +226,6 @@ class PetugasTambahAsetController extends GetxController {
final isFormValid = false.obs; final isFormValid = false.obs;
final isSubmitting = false.obs; final isSubmitting = false.obs;
@override @override
void onClose() { void onClose() {
// Dispose controllers // Dispose controllers
@ -255,11 +266,10 @@ class PetugasTambahAsetController extends GetxController {
if (!anySelected) { if (!anySelected) {
timeOptions[option]?.value = true; timeOptions[option]?.value = true;
} }
validateForm(); validateForm();
} }
// Create a new asset in Supabase // Create a new asset in Supabase
Future<String?> _createAsset( Future<String?> _createAsset(
Map<String, dynamic> assetData, Map<String, dynamic> assetData,
@ -268,7 +278,7 @@ class PetugasTambahAsetController extends GetxController {
try { try {
// Create the asset in the 'aset' table // Create the asset in the 'aset' table
final response = await _asetProvider.createAset(assetData); final response = await _asetProvider.createAset(assetData);
if (response == null || response['id'] == null) { if (response == null || response['id'] == null) {
debugPrint('❌ Failed to create asset: No response or ID from server'); debugPrint('❌ Failed to create asset: No response or ID from server');
return null; return null;
@ -285,7 +295,7 @@ class PetugasTambahAsetController extends GetxController {
harga: sws['harga'], harga: sws['harga'],
maksimalWaktu: sws['maksimal_waktu'], maksimalWaktu: sws['maksimal_waktu'],
); );
if (!success) { if (!success) {
debugPrint('❌ Failed to add satuan waktu sewa: $sws'); debugPrint('❌ Failed to add satuan waktu sewa: $sws');
} }
@ -307,12 +317,12 @@ class PetugasTambahAsetController extends GetxController {
) async { ) async {
try { try {
debugPrint('\n🔄 Starting update for asset ID: $assetId'); debugPrint('\n🔄 Starting update for asset ID: $assetId');
// 1. Extract and remove foto_aset from assetData as it's not in the aset table // 1. Extract and remove foto_aset from assetData as it's not in the aset table
final fotoAsetUrl = assetData['foto_aset']; final fotoAsetUrl = assetData['foto_aset'];
assetData.remove('foto_aset'); assetData.remove('foto_aset');
debugPrint('📝 Asset data prepared for update (without foto_aset)'); debugPrint('📝 Asset data prepared for update (without foto_aset)');
// 2. Update the main asset data (without foto_aset) // 2. Update the main asset data (without foto_aset)
debugPrint('🔄 Updating main asset data...'); debugPrint('🔄 Updating main asset data...');
final success = await _asetProvider.updateAset(assetId, assetData); final success = await _asetProvider.updateAset(assetId, assetData);
@ -326,7 +336,7 @@ class PetugasTambahAsetController extends GetxController {
debugPrint('\n🔄 Updating rental time units...'); debugPrint('\n🔄 Updating rental time units...');
// First, delete existing satuan waktu sewa // First, delete existing satuan waktu sewa
await _asetProvider.deleteSatuanWaktuSewaByAsetId(assetId); await _asetProvider.deleteSatuanWaktuSewaByAsetId(assetId);
// Then add the new ones // Then add the new ones
for (var sws in satuanWaktuSewa) { for (var sws in satuanWaktuSewa) {
debugPrint(' - Adding: ${sws['satuan_waktu']} (${sws['harga']} IDR)'); debugPrint(' - Adding: ${sws['satuan_waktu']} (${sws['harga']} IDR)');
@ -346,21 +356,25 @@ class PetugasTambahAsetController extends GetxController {
...networkImageUrls, ...networkImageUrls,
...selectedImages.map((file) => file.path), ...selectedImages.map((file) => file.path),
]; ];
debugPrint('\n🖼️ Processing photos for asset $assetId'); debugPrint('\n🖼️ Processing photos for asset $assetId');
debugPrint(' - Network URLs: ${networkImageUrls.length}'); debugPrint(' - Network URLs: ${networkImageUrls.length}');
debugPrint(' - Local files: ${selectedImages.length}'); debugPrint(' - Local files: ${selectedImages.length}');
debugPrint(' - Total unique photos: ${allImageUrls.toSet().length} (before deduplication)'); debugPrint(
' - Total unique photos: ${allImageUrls.toSet().length} (before deduplication)',
);
try { try {
// Use updateFotoAset which handles both uploading new photos and updating the database // Use updateFotoAset which handles both uploading new photos and updating the database
final photoSuccess = await _asetProvider.updateFotoAset( final photoSuccess = await _asetProvider.updateFotoAset(
asetId: assetId, asetId: assetId,
fotoUrls: allImageUrls, fotoUrls: allImageUrls,
); );
if (!photoSuccess) { if (!photoSuccess) {
debugPrint('⚠️ Some photos might not have been updated for asset $assetId'); debugPrint(
'⚠️ Some photos might not have been updated for asset $assetId',
);
// We don't fail the whole update if photo update fails // We don't fail the whole update if photo update fails
// as the main asset data has been saved successfully // as the main asset data has been saved successfully
} else { } else {
@ -377,7 +391,6 @@ class PetugasTambahAsetController extends GetxController {
debugPrint('\n✅ Asset update completed successfully for ID: $assetId'); debugPrint('\n✅ Asset update completed successfully for ID: $assetId');
return true; return true;
} catch (e, stackTrace) { } catch (e, stackTrace) {
debugPrint('❌ Error updating asset: $e'); debugPrint('❌ Error updating asset: $e');
debugPrint('Stack trace: $stackTrace'); debugPrint('Stack trace: $stackTrace');
@ -445,30 +458,30 @@ class PetugasTambahAsetController extends GetxController {
// Handle time options and pricing // Handle time options and pricing
final List<Map<String, dynamic>> satuanWaktuSewa = []; final List<Map<String, dynamic>> satuanWaktuSewa = [];
if (timeOptions['Per Jam']?.value == true) { if (timeOptions['Per Jam']?.value == true) {
final hargaPerJam = int.tryParse(pricePerHourController.text) ?? 0; final hargaPerJam = int.tryParse(pricePerHourController.text) ?? 0;
final maxJam = int.tryParse(maxHourController.text) ?? 24; final maxJam = int.tryParse(maxHourController.text) ?? 24;
if (hargaPerJam <= 0) { if (hargaPerJam <= 0) {
throw Exception('Harga per jam harus lebih dari 0'); throw Exception('Harga per jam harus lebih dari 0');
} }
satuanWaktuSewa.add({ satuanWaktuSewa.add({
'satuan_waktu': 'jam', 'satuan_waktu': 'jam',
'harga': hargaPerJam, 'harga': hargaPerJam,
'maksimal_waktu': maxJam, 'maksimal_waktu': maxJam,
}); });
} }
if (timeOptions['Per Hari']?.value == true) { if (timeOptions['Per Hari']?.value == true) {
final hargaPerHari = int.tryParse(pricePerDayController.text) ?? 0; final hargaPerHari = int.tryParse(pricePerDayController.text) ?? 0;
final maxHari = int.tryParse(maxDayController.text) ?? 30; final maxHari = int.tryParse(maxDayController.text) ?? 30;
if (hargaPerHari <= 0) { if (hargaPerHari <= 0) {
throw Exception('Harga per hari harus lebih dari 0'); throw Exception('Harga per hari harus lebih dari 0');
} }
satuanWaktuSewa.add({ satuanWaktuSewa.add({
'satuan_waktu': 'hari', 'satuan_waktu': 'hari',
'harga': hargaPerHari, 'harga': hargaPerHari,
@ -483,7 +496,7 @@ class PetugasTambahAsetController extends GetxController {
// Handle image uploads // Handle image uploads
List<String> imageUrls = []; List<String> imageUrls = [];
if (networkImageUrls.isNotEmpty) { if (networkImageUrls.isNotEmpty) {
// Use existing network URLs // Use existing network URLs
imageUrls = List.from(networkImageUrls); imageUrls = List.from(networkImageUrls);
@ -505,12 +518,12 @@ class PetugasTambahAsetController extends GetxController {
// Create or update the asset // Create or update the asset
bool success; bool success;
String? createdAssetId; String? createdAssetId;
if (isEditing.value && (assetId?.isNotEmpty ?? false)) { if (isEditing.value && (assetId?.isNotEmpty ?? false)) {
// Update existing asset // Update existing asset
debugPrint('🔄 Updating asset with ID: $assetId'); debugPrint('🔄 Updating asset with ID: $assetId');
success = await _updateAsset(assetId!, assetData, satuanWaktuSewa); success = await _updateAsset(assetId!, assetData, satuanWaktuSewa);
// Update all photos if we have any // Update all photos if we have any
if (success && imageUrls.isNotEmpty) { if (success && imageUrls.isNotEmpty) {
await _asetProvider.updateFotoAset( await _asetProvider.updateFotoAset(
@ -523,7 +536,7 @@ class PetugasTambahAsetController extends GetxController {
debugPrint('🔄 Creating new asset'); debugPrint('🔄 Creating new asset');
createdAssetId = await _createAsset(assetData, satuanWaktuSewa); createdAssetId = await _createAsset(assetData, satuanWaktuSewa);
success = createdAssetId != null; success = createdAssetId != null;
// Add all photos for new asset // Add all photos for new asset
if (success && createdAssetId != null && imageUrls.isNotEmpty) { if (success && createdAssetId != null && imageUrls.isNotEmpty) {
await _asetProvider.updateFotoAset( await _asetProvider.updateFotoAset(
@ -537,16 +550,18 @@ class PetugasTambahAsetController extends GetxController {
// Show success message // Show success message
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
isEditing.value ? 'Aset berhasil diperbarui' : 'Aset berhasil ditambahkan', isEditing.value
snackPosition: SnackPosition.BOTTOM, ? 'Aset berhasil diperbarui'
: 'Aset berhasil ditambahkan',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 2),
); );
// Navigate back with success after a short delay // Navigate back with success result instead of using offNamed
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(milliseconds: 500));
Get.back(result: true); Get.offNamed(Routes.PETUGAS_ASET);
} else { } else {
throw Exception('Gagal menyimpan aset'); throw Exception('Gagal menyimpan aset');
} }
@ -557,14 +572,13 @@ class PetugasTambahAsetController extends GetxController {
'Terjadi kesalahan: ${e.toString()}', 'Terjadi kesalahan: ${e.toString()}',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
} }
} }
// Example method to upload images (to be implemented with your backend) // Example method to upload images (to be implemented with your backend)
// Future<List<String>> _uploadImages(List<XFile> images) async { // Future<List<String>> _uploadImages(List<XFile> images) async {
// List<String> urls = []; // List<String> urls = [];
@ -593,7 +607,7 @@ class PetugasTambahAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengambil gambar dari kamera: $e', 'Gagal mengambil gambar dari kamera: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -615,7 +629,7 @@ class PetugasTambahAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memilih gambar dari galeri: $e', 'Gagal memilih gambar dari galeri: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -625,7 +639,9 @@ class PetugasTambahAsetController extends GetxController {
// For demonstration purposes: add sample image // For demonstration purposes: add sample image
void addSampleImage() { void addSampleImage() {
// In a real app, this would open the image picker // In a real app, this would open the image picker
selectedImages.add(XFile('assets/images/sample_asset_${selectedImages.length + 1}.jpg')); selectedImages.add(
XFile('assets/images/sample_asset_${selectedImages.length + 1}.jpg'),
);
validateForm(); validateForm();
} }
} }

View File

@ -5,6 +5,7 @@ import 'package:bumrent_app/app/data/models/paket_model.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:bumrent_app/app/data/providers/aset_provider.dart'; import 'package:bumrent_app/app/data/providers/aset_provider.dart';
import 'dart:io'; import 'dart:io';
import 'package:bumrent_app/app/routes/app_routes.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
class PetugasTambahPaketController extends GetxController { class PetugasTambahPaketController extends GetxController {
@ -47,6 +48,9 @@ class PetugasTambahPaketController extends GetxController {
// New RxBool for editing // New RxBool for editing
final isEditing = false.obs; final isEditing = false.obs;
// New RxBool for viewing (read-only mode)
final isViewing = false.obs;
final timeOptions = {'Per Jam': true.obs, 'Per Hari': false.obs}; final timeOptions = {'Per Jam': true.obs, 'Per Hari': false.obs};
final pricePerHourController = TextEditingController(); final pricePerHourController = TextEditingController();
final maxHourController = TextEditingController(); final maxHourController = TextEditingController();
@ -64,11 +68,13 @@ class PetugasTambahPaketController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
// Ambil flag isEditing dari arguments // Ambil flag isEditing dan isViewing dari arguments
isEditing.value = isEditing.value =
Get.arguments != null && Get.arguments['isEditing'] == true; Get.arguments != null && Get.arguments['isEditing'] == true;
isViewing.value =
Get.arguments != null && Get.arguments['isViewing'] == true;
if (isEditing.value) { if (isEditing.value || isViewing.value) {
final paketArg = Get.arguments['paket']; final paketArg = Get.arguments['paket'];
String? paketId; String? paketId;
if (paketArg != null) { if (paketArg != null) {
@ -230,7 +236,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Pilih aset dan masukkan jumlah', 'Pilih aset dan masukkan jumlah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -251,7 +257,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah harus lebih dari 0', 'Jumlah harus lebih dari 0',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -264,7 +270,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah melebihi stok yang tersedia', 'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -286,7 +292,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Item berhasil ditambahkan ke paket', 'Item berhasil ditambahkan ke paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -300,7 +306,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Pilih aset dan masukkan jumlah', 'Pilih aset dan masukkan jumlah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -321,7 +327,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah harus lebih dari 0', 'Jumlah harus lebih dari 0',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -349,7 +355,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah melebihi stok yang tersedia', 'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -362,7 +368,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Jumlah melebihi stok yang tersedia', 'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -385,7 +391,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Item berhasil diperbarui', 'Item berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -402,7 +408,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Dihapus', 'Dihapus',
'Item berhasil dihapus dari paket', 'Item berhasil dihapus dari paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
colorText: Colors.white, colorText: Colors.white,
); );
@ -538,13 +544,13 @@ class PetugasTambahPaketController extends GetxController {
} }
// Sukses // Sukses
Get.back(); Get.offNamed(Routes.PETUGAS_PAKET);
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Paket berhasil diperbarui', 'Paket berhasil diperbarui',
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} else { } else {
// --- ADD LOGIC --- // --- ADD LOGIC ---
@ -611,13 +617,13 @@ class PetugasTambahPaketController extends GetxController {
} }
} }
// Sukses // Sukses
Get.back(); Get.back(result: true);
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Paket berhasil ditambahkan', 'Paket berhasil ditambahkan',
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} catch (e) { } catch (e) {
@ -627,7 +633,7 @@ class PetugasTambahPaketController extends GetxController {
'Terjadi kesalahan: \\${e.toString()}', 'Terjadi kesalahan: \\${e.toString()}',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
@ -810,7 +816,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengambil gambar dari kamera: $e', 'Gagal mengambil gambar dari kamera: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -833,7 +839,7 @@ class PetugasTambahPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memilih gambar dari galeri: $e', 'Gagal memilih gambar dari galeri: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -476,7 +476,7 @@ class ListPetugasMitraView extends GetView<ListPetugasMitraController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Harap isi semua data', 'Harap isi semua data',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -600,7 +600,7 @@ class ListPetugasMitraView extends GetView<ListPetugasMitraController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Harap isi semua data', 'Harap isi semua data',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -203,7 +203,7 @@ class ListTagihanPeriodeView extends GetView<ListTagihanPeriodeController> {
backgroundColor: Colors.orange.withOpacity(0.1), backgroundColor: Colors.orange.withOpacity(0.1),
colorText: Colors.orange.shade800, colorText: Colors.orange.shade800,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
margin: const EdgeInsets.all(8), margin: const EdgeInsets.all(8),
); );
}, },

View File

@ -81,11 +81,15 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: () { onPressed: () {
// Navigate to PetugasTambahAsetView in add mode // Navigate to PetugasTambahAsetView in add mode and refresh data when returning
Get.toNamed( Get.toNamed(
Routes.PETUGAS_TAMBAH_ASET, Routes.PETUGAS_TAMBAH_ASET,
arguments: {'isEditing': false, 'assetData': null}, arguments: {'isEditing': false, 'assetData': null},
); )?.then((_) {
// Refresh data when returning from tambah_aset page
debugPrint('Returning from tambah aset page, refreshing data...');
controller.loadAsetData();
});
}, },
backgroundColor: AppColorsPetugas.babyBlueBright, backgroundColor: AppColorsPetugas.babyBlueBright,
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto), icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
@ -456,7 +460,7 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID Aset tidak valid', 'ID Aset tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
return; return;
} }
@ -467,7 +471,13 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
'isEditing': true, 'isEditing': true,
'assetId': assetId, 'assetId': assetId,
}, },
); )?.then((_) {
// Refresh data when returning from edit page
debugPrint(
'Returning from edit aset page, refreshing data...',
);
controller.loadAsetData();
});
}, },
child: Container( child: Container(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(5),
@ -670,235 +680,16 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
} }
} }
void _showAddEditAssetDialog( void _showAddEditAssetDialog(BuildContext context) {
BuildContext context, { // Navigate to PetugasTambahAsetView in add mode and refresh data when returning
Map<String, dynamic>? aset, Get.toNamed(
}) { Routes.PETUGAS_TAMBAH_ASET,
final isEditing = aset != null; arguments: {'isEditing': false, 'assetData': null},
)?.then((_) {
// In a real app, this would have proper form handling with controllers // Refresh data when returning from tambah_aset page
showDialog( debugPrint('Returning from tambah aset page, refreshing data...');
context: context, controller.loadAsetData();
builder: (context) { });
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
child: Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlue,
shape: BoxShape.circle,
),
child: Icon(
isEditing ? Icons.edit : Icons.add,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
isEditing ? 'Edit Aset' : 'Tambah Aset Baru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 4),
Text(
'Silakan lengkapi form di bawah ini',
style: TextStyle(
fontSize: 14,
color: AppColorsPetugas.textSecondary,
),
),
],
),
),
],
),
const SizedBox(height: 24),
// Mock form - In a real app this would have actual form fields
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueBright,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColorsPetugas.babyBlue),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Form pengelolaan aset akan ditampilkan di sini dengan field untuk:',
style: TextStyle(
fontSize: 14,
color: AppColorsPetugas.textPrimary,
),
),
const SizedBox(height: 16),
_buildMockFormField('Nama Aset', 'Contoh: Meja Rapat'),
_buildMockFormField('Kategori', 'Pilih kategori aset'),
_buildMockFormField(
'Harga',
'Masukkan harga per unit/periode',
),
_buildMockFormField(
'Satuan',
'Contoh: per hari, per bulan',
),
_buildMockFormField('Stok', 'Jumlah unit tersedia'),
_buildMockFormField(
'Deskripsi',
'Keterangan lengkap aset',
),
_buildMockToggle(
'Status Ketersediaan',
isEditing && aset?['tersedia'] == true,
),
],
),
),
const SizedBox(height: 24),
// Action buttons
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'Batal',
style: TextStyle(
color: AppColorsPetugas.textSecondary,
fontWeight: FontWeight.w600,
),
),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: () {
Navigator.pop(context);
// In a real app, we would save the form data
Get.snackbar(
isEditing ? 'Aset Diperbarui' : 'Aset Ditambahkan',
isEditing
? 'Aset berhasil diperbarui'
: 'Aset baru berhasil ditambahkan',
backgroundColor: AppColorsPetugas.success,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
margin: const EdgeInsets.all(16),
borderRadius: 10,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColorsPetugas.blueGrotto,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(isEditing ? 'Simpan' : 'Tambah'),
),
],
),
],
),
),
);
},
);
}
Widget _buildMockFormField(String label, String hint) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textPrimary,
),
),
const SizedBox(height: 6),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppColorsPetugas.babyBlue),
),
child: Row(
children: [
Expanded(
child: Text(
hint,
style: TextStyle(
fontSize: 14,
color: AppColorsPetugas.textLight,
),
),
),
Icon(
Icons.keyboard_arrow_down,
color: AppColorsPetugas.textSecondary,
size: 20,
),
],
),
),
],
),
);
}
Widget _buildMockToggle(String label, bool value) {
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textPrimary,
),
),
Switch(
value: value,
onChanged: (_) {},
activeColor: AppColorsPetugas.blueGrotto,
),
],
),
);
} }
void _showDeleteConfirmation( void _showDeleteConfirmation(

View File

@ -1023,7 +1023,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Status sewa telah diubah menjadi SELESAI', 'Status sewa telah diubah menjadi SELESAI',
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -1054,7 +1054,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Status sewa telah diubah menjadi DIKEMBALIKAN', 'Status sewa telah diubah menjadi DIKEMBALIKAN',
backgroundColor: Colors.teal, backgroundColor: Colors.teal,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -1127,7 +1127,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Status sewa telah diubah menjadi DIBATALKAN', 'Status sewa telah diubah menjadi DIBATALKAN',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -1381,7 +1381,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Status pengajuan diubah menjadi Periksa Pembayaran', 'Status pengajuan diubah menjadi Periksa Pembayaran',
backgroundColor: Colors.amber.shade700, backgroundColor: Colors.amber.shade700,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
break; break;
@ -1394,7 +1394,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Pengajuan sewa aset telah disetujui', 'Pengajuan sewa aset telah disetujui',
backgroundColor: Colors.green.shade600, backgroundColor: Colors.green.shade600,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
break; break;
@ -1407,7 +1407,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Permintaan pembayaran denda telah dikirim', 'Permintaan pembayaran denda telah dikirim',
backgroundColor: Colors.deepOrange, backgroundColor: Colors.deepOrange,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
break; break;
@ -1420,7 +1420,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Status pengajuan diubah menjadi Periksa Denda', 'Status pengajuan diubah menjadi Periksa Denda',
backgroundColor: Colors.red.shade600, backgroundColor: Colors.red.shade600,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
break; break;
@ -1433,7 +1433,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Aset telah dikembalikan dan sewa telah selesai', 'Aset telah dikembalikan dan sewa telah selesai',
backgroundColor: Colors.green.shade600, backgroundColor: Colors.green.shade600,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
break; break;
@ -1476,7 +1476,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Status sewa telah diubah menjadi DIBATALKAN', 'Status sewa telah diubah menjadi DIBATALKAN',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -1595,7 +1595,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Status sewa diubah menjadi Dikembalikan', 'Status sewa diubah menjadi Dikembalikan',
backgroundColor: Colors.teal, backgroundColor: Colors.teal,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
), ),
@ -1628,7 +1628,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Permintaan pembayaran denda telah dikirim', 'Permintaan pembayaran denda telah dikirim',
backgroundColor: Colors.deepOrange, backgroundColor: Colors.deepOrange,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
), ),
@ -1661,7 +1661,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Sewa aset telah selesai', 'Sewa aset telah selesai',
backgroundColor: Colors.purple, backgroundColor: Colors.purple,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}; };
} else if (status == 'Dikembalikan') { } else if (status == 'Dikembalikan') {
@ -1709,7 +1709,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Sewa aset telah selesai', 'Sewa aset telah selesai',
backgroundColor: Colors.purple, backgroundColor: Colors.purple,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
icon: const Icon(Icons.task_alt_outlined), icon: const Icon(Icons.task_alt_outlined),
@ -1817,7 +1817,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Bukti sewa dalam format PDF sedang diunduh', 'Bukti sewa dalam format PDF sedang diunduh',
backgroundColor: AppColorsPetugas.blueGrotto, backgroundColor: AppColorsPetugas.blueGrotto,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
), ),
@ -1837,7 +1837,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Bukti sewa dalam format JPG sedang diunduh', 'Bukti sewa dalam format JPG sedang diunduh',
backgroundColor: AppColorsPetugas.blueGrotto, backgroundColor: AppColorsPetugas.blueGrotto,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
), ),
@ -1857,7 +1857,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Data sewa dalam format XLSX sedang diunduh', 'Data sewa dalam format XLSX sedang diunduh',
backgroundColor: AppColorsPetugas.blueGrotto, backgroundColor: AppColorsPetugas.blueGrotto,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
), ),
@ -1963,7 +1963,8 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
width: double.infinity, width: double.infinity,
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () { onPressed: () {
nominalController.text = sewa.totalTagihan.toString(); nominalController.text =
sewa.totalTagihan.toInt().toString();
}, },
icon: Icon( icon: Icon(
Icons.payments_outlined, Icons.payments_outlined,
@ -2456,7 +2457,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Nominal denda harus diisi', 'Nominal denda harus diisi',
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
return; return;
} }
@ -2501,7 +2502,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
'Permintaan pembayaran denda telah dikirim', 'Permintaan pembayaran denda telah dikirim',
backgroundColor: Colors.deepOrange, backgroundColor: Colors.deepOrange,
colorText: Colors.white, colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
icon: const Icon(Icons.check_circle), icon: const Icon(Icons.check_circle),

View File

@ -567,7 +567,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur tambah rekening bank sedang dalam pengembangan', 'Fitur tambah rekening bank sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -639,7 +639,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur edit rekening bank sedang dalam pengembangan', 'Fitur edit rekening bank sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -689,7 +689,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur hapus rekening bank sedang dalam pengembangan', 'Fitur hapus rekening bank sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -765,7 +765,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur tambah mitra sedang dalam pengembangan', 'Fitur tambah mitra sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -845,7 +845,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur edit mitra sedang dalam pengembangan', 'Fitur edit mitra sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -895,7 +895,7 @@ class PetugasManajemenBumdesView
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur hapus mitra sedang dalam pengembangan', 'Fitur hapus mitra sedang dalam pengembangan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(

View File

@ -55,11 +55,17 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
), ),
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: onPressed: () async {
() => Get.toNamed( final result = await Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET, Routes.PETUGAS_TAMBAH_PAKET,
arguments: {'isEditing': false}, arguments: {'isEditing': false},
), );
// Refresh the package list if a package was added
if (result == true) {
controller.loadPaketData();
}
},
label: Text( label: Text(
'Tambah Paket', 'Tambah Paket',
style: TextStyle( style: TextStyle(
@ -142,11 +148,17 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: onPressed: () async {
() => Get.toNamed( final result = await Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET, Routes.PETUGAS_TAMBAH_PAKET,
arguments: {'isEditing': false}, arguments: {'isEditing': false},
), );
// Refresh the package list if a package was added
if (result == true) {
controller.loadPaketData();
}
},
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text('Tambah Paket'), label: const Text('Tambah Paket'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@ -376,6 +388,21 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () async {
final result = await Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET,
arguments: {
'isEditing': false,
'isViewing': true,
'paket': paket,
},
);
// Refresh the package list if data was modified
if (result == true) {
controller.loadPaketData();
}
},
child: Row( child: Row(
children: [ children: [
// Paket image or icon // Paket image or icon

View File

@ -111,6 +111,27 @@ class _PetugasPenyewaViewState extends State<PetugasPenyewaView>
), ),
), ),
), ),
actions: [
// Tambahkan indikator refresh
Obx(
() =>
controller.isRefreshing.value
? Padding(
padding: const EdgeInsets.only(right: 16.0),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
),
)
: SizedBox.shrink(),
),
],
), ),
drawer: PetugasSideNavbar(controller: dashboardController), drawer: PetugasSideNavbar(controller: dashboardController),
drawerEdgeDragWidth: 60, drawerEdgeDragWidth: 60,
@ -148,7 +169,13 @@ class _PetugasPenyewaViewState extends State<PetugasPenyewaView>
} }
if (controller.filteredPenyewaList.isEmpty) { if (controller.filteredPenyewaList.isEmpty) {
return _buildEmptyState(); return RefreshIndicator(
onRefresh: () async {
await controller.refreshPenyewaList();
},
color: AppColorsPetugas.blueGrotto,
child: _buildEmptyStateWithScrollView(),
);
} }
return _buildPenyewaList(); return _buildPenyewaList();
@ -282,208 +309,234 @@ class _PetugasPenyewaViewState extends State<PetugasPenyewaView>
); );
} }
// Widget untuk menampilkan empty state dengan ScrollView untuk mendukung RefreshIndicator
Widget _buildEmptyStateWithScrollView() {
return ListView(
physics: const AlwaysScrollableScrollPhysics(),
children: [
SizedBox(
height: MediaQuery.of(Get.context!).size.height * 0.7,
child: _buildEmptyState(),
),
],
);
}
Widget _buildPenyewaList() { Widget _buildPenyewaList() {
return ListView.builder( return RefreshIndicator(
padding: const EdgeInsets.all(16), onRefresh: () async {
itemCount: controller.filteredPenyewaList.length, await controller.refreshPenyewaList();
itemBuilder: (context, index) { },
final penyewa = controller.filteredPenyewaList[index]; color: AppColorsPetugas.blueGrotto,
return Container( child: ListView.builder(
margin: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( itemCount: controller.filteredPenyewaList.length,
color: Colors.white, itemBuilder: (context, index) {
borderRadius: BorderRadius.circular(16), final penyewa = controller.filteredPenyewaList[index];
boxShadow: [ return Container(
BoxShadow( margin: const EdgeInsets.only(bottom: 16),
color: Colors.black.withOpacity(0.05), decoration: BoxDecoration(
blurRadius: 10, color: Colors.white,
offset: const Offset(0, 4),
),
],
),
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(16),
child: InkWell(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: Padding( boxShadow: [
padding: const EdgeInsets.all(0), BoxShadow(
child: Column( color: Colors.black.withOpacity(0.05),
crossAxisAlignment: CrossAxisAlignment.start, blurRadius: 10,
children: [ offset: const Offset(0, 4),
// Header with avatar and badge ),
Container( ],
padding: const EdgeInsets.all(16), ),
decoration: BoxDecoration( child: Material(
color: AppColorsPetugas.babyBlueLight.withOpacity(0.2), color: Colors.transparent,
borderRadius: const BorderRadius.only( borderRadius: BorderRadius.circular(16),
topLeft: Radius.circular(16), child: InkWell(
topRight: Radius.circular(16), borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with avatar and badge
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight.withOpacity(
0.2,
),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
child: Row(
children: [
// Avatar with border
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white,
width: 2,
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: CircleAvatar(
radius: 24,
backgroundColor: AppColorsPetugas.babyBlueLight,
backgroundImage:
penyewa['avatar'] != null &&
penyewa['avatar']
.toString()
.isNotEmpty
? NetworkImage(penyewa['avatar'])
: null,
child:
penyewa['avatar'] == null ||
penyewa['avatar'].toString().isEmpty
? const Icon(
Icons.person,
color: Colors.white,
)
: null,
),
),
const SizedBox(width: 16),
// Name and email
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
penyewa['nama_lengkap'] ??
'Nama tidak tersedia',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.email_outlined,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Expanded(
child: Text(
penyewa['email'] ??
'Email tidak tersedia',
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
_buildStatusBadge(penyewa['status']),
],
), ),
), ),
child: Row(
children: [
// Avatar with border
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: CircleAvatar(
radius: 24,
backgroundColor: AppColorsPetugas.babyBlueLight,
backgroundImage:
penyewa['avatar'] != null &&
penyewa['avatar']
.toString()
.isNotEmpty
? NetworkImage(penyewa['avatar'])
: null,
child:
penyewa['avatar'] == null ||
penyewa['avatar'].toString().isEmpty
? const Icon(
Icons.person,
color: Colors.white,
)
: null,
),
),
const SizedBox(width: 16),
// Name and email
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
penyewa['nama_lengkap'] ??
'Nama tidak tersedia',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 4),
Row(
children: [
Icon(
Icons.email_outlined,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Expanded(
child: Text(
penyewa['email'] ??
'Email tidak tersedia',
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
_buildStatusBadge(penyewa['status']),
],
),
),
// Content section // Content section
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Show additional info only for Aktif and Ditangguhkan tabs // Show additional info only for Aktif and Ditangguhkan tabs
if (controller.currentTabIndex.value != 0) ...[ if (controller.currentTabIndex.value != 0) ...[
Row( Row(
children: [ children: [
_buildInfoChip( _buildInfoChip(
Icons.credit_card_outlined, Icons.credit_card_outlined,
'NIK: ${penyewa['nik'] ?? 'Tidak tersedia'}', 'NIK: ${penyewa['nik'] ?? 'Tidak tersedia'}',
),
const SizedBox(width: 8),
_buildInfoChip(
Icons.phone_outlined,
penyewa['no_hp'] ?? 'No. HP tidak tersedia',
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_buildInfoTile(
'Total Sewa',
penyewa['total_sewa']?.toString() ?? '0',
Icons.shopping_bag_outlined,
AppColorsPetugas.blueGrotto,
),
if (controller.currentTabIndex.value == 1 ||
controller.currentTabIndex.value == 2)
_buildActionChip(
label: 'Lihat Detail',
color: AppColorsPetugas.blueGrotto,
icon: Icons.visibility,
onTap: () {
Get.toNamed(
Routes.PETUGAS_DETAIL_PENYEWA,
arguments: {
'userId': penyewa['user_id'],
},
);
},
), ),
], const SizedBox(width: 8),
), _buildInfoChip(
], Icons.phone_outlined,
penyewa['no_hp'] ?? 'No. HP tidak tersedia',
),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
_buildInfoTile(
'Total Sewa',
penyewa['total_sewa']?.toString() ?? '0',
Icons.shopping_bag_outlined,
AppColorsPetugas.blueGrotto,
),
if (controller.currentTabIndex.value == 1 ||
controller.currentTabIndex.value == 2)
_buildActionChip(
label: 'Lihat Detail',
color: AppColorsPetugas.blueGrotto,
icon: Icons.visibility,
onTap: () {
Get.toNamed(
Routes.PETUGAS_DETAIL_PENYEWA,
arguments: {
'userId': penyewa['user_id'],
},
);
},
),
],
),
],
// Add "Detail" button for Verifikasi tab // Add "Detail" button for Verifikasi tab
if (controller.currentTabIndex.value == 0) ...[ if (controller.currentTabIndex.value == 0) ...[
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment:
children: [ MainAxisAlignment.spaceBetween,
Expanded( children: [
child: _buildActionChip( Expanded(
label: 'Lihat Detail & Verifikasi', child: _buildActionChip(
color: AppColorsPetugas.blueGrotto, label: 'Lihat Detail & Verifikasi',
icon: Icons.visibility, color: AppColorsPetugas.blueGrotto,
onTap: () { icon: Icons.visibility,
Get.toNamed( onTap: () {
Routes.PETUGAS_DETAIL_PENYEWA, Get.toNamed(
arguments: { Routes.PETUGAS_DETAIL_PENYEWA,
'userId': penyewa['user_id'], arguments: {
}, 'userId': penyewa['user_id'],
); },
}, );
},
),
), ),
), ],
], ),
), ],
], ],
], ),
), ),
), ],
], ),
), ),
), ),
), ),
), );
); },
}, ),
); );
} }

View File

@ -15,7 +15,9 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
appBar: AppBar( appBar: AppBar(
title: Obx( title: Obx(
() => Text( () => Text(
controller.isEditing.value ? 'Edit Paket' : 'Tambah Paket', controller.isViewing.value
? 'Detail Paket'
: (controller.isEditing.value ? 'Edit Paket' : 'Tambah Paket'),
style: const TextStyle(fontWeight: FontWeight.w600), style: const TextStyle(fontWeight: FontWeight.w600),
), ),
), ),
@ -168,15 +170,16 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
'Item Paket', 'Item Paket',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
ElevatedButton.icon( if (!controller.isViewing.value)
onPressed: () => _showAddItemDialog(), ElevatedButton.icon(
icon: const Icon(Icons.add), onPressed: () => _showAddItemDialog(),
label: const Text('Tambah Item'), icon: const Icon(Icons.add),
style: ElevatedButton.styleFrom( label: const Text('Tambah Item'),
backgroundColor: AppColorsPetugas.babyBlueLight, style: ElevatedButton.styleFrom(
foregroundColor: AppColorsPetugas.blueGrotto, backgroundColor: AppColorsPetugas.babyBlueLight,
foregroundColor: AppColorsPetugas.blueGrotto,
),
), ),
),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@ -208,26 +211,33 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
Text('Stok tersedia: ${item['stok']}'), Text('Stok tersedia: ${item['stok']}'),
], ],
), ),
trailing: Row( trailing:
mainAxisSize: MainAxisSize.min, controller.isViewing.value
children: [ ? null
IconButton( : Row(
icon: const Icon( mainAxisSize: MainAxisSize.min,
Icons.edit, children: [
color: Colors.blue, IconButton(
), icon: const Icon(
onPressed: () => _showEditItemDialog(index), Icons.edit,
), color: Colors.blue,
IconButton( ),
icon: const Icon( onPressed:
Icons.delete, () =>
color: Colors.red, _showEditItemDialog(index),
), ),
onPressed: IconButton(
() => controller.removeItem(index), icon: const Icon(
), Icons.delete,
], color: Colors.red,
), ),
onPressed:
() => controller.removeItem(
index,
),
),
],
),
), ),
); );
}, },
@ -526,6 +536,9 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
String? prefixText, String? prefixText,
IconData? prefixIcon, IconData? prefixIcon,
}) { }) {
final petugasController =
this.controller; // Reference to PetugasTambahPaketController
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -539,7 +552,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
color: AppColorsPetugas.textPrimary, color: AppColorsPetugas.textPrimary,
), ),
), ),
if (isRequired) ...[ if (isRequired && !petugasController.isViewing.value) ...[
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'*', '*',
@ -553,43 +566,55 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
TextFormField( Obx(
controller: controller, () => TextFormField(
maxLines: maxLines, controller: controller,
keyboardType: keyboardType, maxLines: maxLines,
inputFormatters: inputFormatters, keyboardType: keyboardType,
decoration: InputDecoration( inputFormatters: inputFormatters,
hintText: hint, readOnly: petugasController.isViewing.value,
hintStyle: TextStyle(color: AppColorsPetugas.textLight), style: TextStyle(
filled: true, color:
fillColor: AppColorsPetugas.babyBlueBright, petugasController.isViewing.value
prefixText: prefixText, ? AppColorsPetugas.textSecondary
prefixIcon: : AppColorsPetugas.textPrimary,
prefixIcon != null
? Icon(
prefixIcon,
size: 20,
color: AppColorsPetugas.textSecondary,
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
), ),
enabledBorder: OutlineInputBorder( decoration: InputDecoration(
borderRadius: BorderRadius.circular(10), hintText: hint,
borderSide: BorderSide.none, hintStyle: TextStyle(color: AppColorsPetugas.textLight),
), filled: true,
focusedBorder: OutlineInputBorder( fillColor:
borderRadius: BorderRadius.circular(10), petugasController.isViewing.value
borderSide: BorderSide( ? AppColorsPetugas.babyBlueLight
color: AppColorsPetugas.blueGrotto, : AppColorsPetugas.babyBlueBright,
width: 1.5, prefixText: prefixText,
prefixIcon:
prefixIcon != null
? Icon(
prefixIcon,
size: 20,
color: AppColorsPetugas.textSecondary,
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(
color: AppColorsPetugas.blueGrotto,
width: 1.5,
),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
), ),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
), ),
), ),
), ),
@ -646,7 +671,10 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
vertical: 12, vertical: 12,
), ),
filled: true, filled: true,
fillColor: AppColorsPetugas.babyBlueBright, fillColor:
controller.isViewing.value
? AppColorsPetugas.babyBlueLight
: AppColorsPetugas.babyBlueBright,
), ),
items: items:
options.map((option) { options.map((option) {
@ -661,9 +689,12 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
), ),
); );
}).toList(), }).toList(),
onChanged: (value) { onChanged:
if (value != null) onChanged(value); controller.isViewing.value
}, ? null // Disable in view-only mode
: (value) {
if (value != null) onChanged(value);
},
icon: Icon( icon: Icon(
Icons.keyboard_arrow_down_rounded, Icons.keyboard_arrow_down_rounded,
color: AppColorsPetugas.blueGrotto, color: AppColorsPetugas.blueGrotto,
@ -717,41 +748,42 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
runSpacing: 12, runSpacing: 12,
children: [ children: [
// Add button // Add button
GestureDetector( if (!controller.isViewing.value)
onTap: _showImageSourceOptions, GestureDetector(
child: Container( onTap: _showImageSourceOptions,
width: 100, child: Container(
height: 100, width: 100,
decoration: BoxDecoration( height: 100,
color: AppColorsPetugas.babyBlueBright, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10), color: AppColorsPetugas.babyBlueBright,
border: Border.all( borderRadius: BorderRadius.circular(10),
color: AppColorsPetugas.babyBlue, border: Border.all(
width: 1, color: AppColorsPetugas.babyBlue,
style: BorderStyle.solid, width: 1,
style: BorderStyle.solid,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate_outlined,
color: AppColorsPetugas.blueGrotto,
size: 32,
),
const SizedBox(height: 8),
Text(
'Tambah Foto',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.blueGrotto,
fontWeight: FontWeight.w500,
),
),
],
), ),
), ),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.add_photo_alternate_outlined,
color: AppColorsPetugas.blueGrotto,
size: 32,
),
const SizedBox(height: 8),
Text(
'Tambah Foto',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.blueGrotto,
fontWeight: FontWeight.w500,
),
),
],
),
), ),
),
// Image previews // Image previews
...List<Widget>.generate(controller.selectedImages.length, ( ...List<Widget>.generate(controller.selectedImages.length, (
index, index,
@ -811,21 +843,24 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
Positioned( Positioned(
top: 4, top: 4,
right: 4, right: 4,
child: InkWell( child:
onTap: () => controller.removeImage(index), !controller.isViewing.value
child: Container( ? InkWell(
padding: const EdgeInsets.all(4), onTap: () => controller.removeImage(index),
decoration: const BoxDecoration( child: Container(
color: Colors.white, padding: const EdgeInsets.all(4),
shape: BoxShape.circle, decoration: const BoxDecoration(
), color: Colors.white,
child: const Icon( shape: BoxShape.circle,
Icons.close, ),
size: 18, child: const Icon(
color: Colors.red, Icons.close,
), size: 18,
), color: Colors.red,
), ),
),
)
: const SizedBox.shrink(),
), ),
], ],
); );
@ -949,67 +984,97 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
), ),
], ],
), ),
child: Row( child: Obx(() {
children: [ // In view-only mode, just show a back button
OutlinedButton.icon( if (controller.isViewing.value) {
onPressed: () => Get.back(), return SizedBox(
icon: const Icon(Icons.arrow_back), width: double.infinity,
label: const Text('Batal'), child: ElevatedButton.icon(
style: OutlinedButton.styleFrom( onPressed: () => Get.back(),
foregroundColor: AppColorsPetugas.textSecondary, icon: const Icon(Icons.arrow_back),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), label: const Text('Kembali'),
side: BorderSide(color: AppColorsPetugas.divider), style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder( backgroundColor: AppColorsPetugas.blueGrotto,
borderRadius: BorderRadius.circular(10), foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
), ),
), ),
), );
const SizedBox(width: 16), }
Expanded(
child: Obx(() { // For edit/add mode, show cancel and save buttons
final isValid = controller.isFormValid.value; return Row(
final isSubmitting = controller.isSubmitting.value; children: [
return ElevatedButton.icon( OutlinedButton.icon(
onPressed: onPressed: () => Get.back(),
controller.isFormChanged.value && !isSubmitting icon: const Icon(Icons.arrow_back),
? controller.savePaket label: const Text('Batal'),
: null, style: OutlinedButton.styleFrom(
icon: foregroundColor: AppColorsPetugas.textSecondary,
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 16,
),
side: BorderSide(color: AppColorsPetugas.divider),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Obx(() {
final isValid = controller.isFormValid.value;
final isSubmitting = controller.isSubmitting.value;
return ElevatedButton.icon(
onPressed:
controller.isFormChanged.value && !isSubmitting
? controller.savePaket
: null,
icon:
isSubmitting
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.save),
label: Text(
isSubmitting isSubmitting
? const SizedBox( ? 'Menyimpan...'
width: 24, : (controller.isEditing.value
height: 24, ? 'Simpan Paket'
child: CircularProgressIndicator( : 'Tambah Paket'),
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.save),
label: Text(
isSubmitting
? 'Menyimpan...'
: (controller.isEditing.value
? 'Simpan Paket'
: 'Tambah Paket'),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColorsPetugas.blueGrotto,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
), ),
shape: RoundedRectangleBorder( style: ElevatedButton.styleFrom(
borderRadius: BorderRadius.circular(12), backgroundColor: AppColorsPetugas.blueGrotto,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
disabledBackgroundColor: AppColorsPetugas.textLight,
), ),
disabledBackgroundColor: AppColorsPetugas.textLight, );
), }),
); ),
}), ],
), );
], }),
),
); );
} }
@ -1035,7 +1100,10 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
() => Material( () => Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () => controller.toggleTimeOption(option), onTap:
controller.isViewing.value
? null
: () => controller.toggleTimeOption(option),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@ -1173,12 +1241,22 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
controller: priceController, controller: priceController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
readOnly: controller.isViewing.value,
style: TextStyle(
color:
controller.isViewing.value
? AppColorsPetugas.textSecondary
: AppColorsPetugas.textPrimary,
),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Masukkan harga', hintText: 'Masukkan harga',
hintStyle: TextStyle(color: AppColorsPetugas.textLight), hintStyle: TextStyle(color: AppColorsPetugas.textLight),
prefixText: 'Rp ', prefixText: 'Rp ',
filled: true, filled: true,
fillColor: AppColorsPetugas.babyBlueBright, fillColor:
controller.isViewing.value
? AppColorsPetugas.babyBlueLight
: AppColorsPetugas.babyBlueBright,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 12, vertical: 12,
@ -1210,11 +1288,21 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
controller: maxController, controller: maxController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
readOnly: controller.isViewing.value,
style: TextStyle(
color:
controller.isViewing.value
? AppColorsPetugas.textSecondary
: AppColorsPetugas.textPrimary,
),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Opsional', hintText: 'Opsional',
hintStyle: TextStyle(color: AppColorsPetugas.textLight), hintStyle: TextStyle(color: AppColorsPetugas.textLight),
filled: true, filled: true,
fillColor: AppColorsPetugas.babyBlueBright, fillColor:
controller.isViewing.value
? AppColorsPetugas.babyBlueLight
: AppColorsPetugas.babyBlueBright,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 12, horizontal: 12,
vertical: 12, vertical: 12,

View File

@ -41,7 +41,7 @@ class PetugasMitraDashboardController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal keluar dari aplikasi', 'Gagal keluar dari aplikasi',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }
} }

View File

@ -185,7 +185,7 @@ class OrderSewaAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
errorMessage.value, errorMessage.value,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}); });
} }
@ -211,7 +211,7 @@ class OrderSewaAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Tidak dapat menampilkan data - data tidak tersedia', 'Tidak dapat menampilkan data - data tidak tersedia',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.amber, backgroundColor: Colors.amber,
colorText: Colors.black, colorText: Colors.black,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),
@ -231,6 +231,9 @@ class OrderSewaAsetController extends GetxController {
debugPrint('💾 Saved asetId to GetStorage: ${asetId.value}'); debugPrint('💾 Saved asetId to GetStorage: ${asetId.value}');
} }
// Bersihkan data controller
clearData();
super.onClose(); super.onClose();
} }
@ -445,6 +448,9 @@ class OrderSewaAsetController extends GetxController {
void onBackPressed() { void onBackPressed() {
debugPrint('🔙 Back button pressed in OrderSewaAsetView'); debugPrint('🔙 Back button pressed in OrderSewaAsetView');
// Bersihkan data controller sebelum kembali
clearData();
try { try {
// Try to use the navigation service // Try to use the navigation service
navigationService.backFromOrderSewaAset(); navigationService.backFromOrderSewaAset();
@ -2287,7 +2293,7 @@ class OrderSewaAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
message, message,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -2299,7 +2305,7 @@ class OrderSewaAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
message, message,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -2767,7 +2773,7 @@ class OrderSewaAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal membuat pesanan', 'Gagal membuat pesanan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -2777,7 +2783,7 @@ class OrderSewaAsetController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan: $e', 'Terjadi kesalahan: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -3537,4 +3543,43 @@ class OrderSewaAsetController extends GetxController {
isLoadingBookings(false); isLoadingBookings(false);
} }
} }
// Clear controller data
void clearData() {
aset.value = null;
isLoading.value = true;
hasError.value = false;
errorMessage.value = '';
asetId.value = '';
isAset.value = true;
isPaket.value = false;
assetPhotos.clear();
currentPhotoIndex.value = 0;
isPhotosLoading.value = false;
selectedSatuanWaktu.value = null;
duration.value = 1;
totalPrice.value = 0;
jumlahUnit.value = 1;
maxUnit.value = 1;
selectedDate.value = '';
startHour.value = -1;
endHour.value = -1;
formattedTimeRange.value = '';
startDate.value = null;
endDate.value = null;
formattedDateRange.value = '';
bookedDates.clear();
isLoadingBookedDates.value = false;
maxDayLimit.value = 0;
availableHours.clear();
bookedHours.clear();
selectedHours.clear();
bookedHoursList.clear();
isLoadingBookings.value = false;
hourlyInventory.clear();
unavailableDatesForHourly.clear();
paketItems.clear();
isPaketItemsLoaded.value = false;
paketId.value = '';
}
} }

View File

@ -141,7 +141,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memuat data paket', 'Gagal memuat data paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -152,7 +152,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memuat data paket', 'Terjadi kesalahan saat memuat data paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -166,10 +166,10 @@ class OrderSewaPaketController extends GetxController {
try { try {
debugPrint('🔄 Processing paket data for ID: $id'); debugPrint('🔄 Processing paket data for ID: $id');
debugPrint('📦 Raw paket data type: ${rawPaket.runtimeType}'); debugPrint('📦 Raw paket data type: ${rawPaket.runtimeType}');
// Initialize loadedPaket with a default value // Initialize loadedPaket with a default value
late final PaketModel loadedPaket; late final PaketModel loadedPaket;
try { try {
// Handle Map directly - pakets from getPakets() are always maps // Handle Map directly - pakets from getPakets() are always maps
loadedPaket = PaketModel.fromMap(rawPaket); loadedPaket = PaketModel.fromMap(rawPaket);
@ -183,10 +183,16 @@ class OrderSewaPaketController extends GetxController {
deskripsi: getPaketDeskripsi(rawPaket), deskripsi: getPaketDeskripsi(rawPaket),
harga: getPaketHarga(rawPaket), harga: getPaketHarga(rawPaket),
kuantitas: getPaketKuantitas(rawPaket), kuantitas: getPaketKuantitas(rawPaket),
foto: const <String>[], // Initialize with empty list, will be populated later foto:
const <
String
>[], // Initialize with empty list, will be populated later
satuanWaktuSewa: getPaketSatuanWaktuSewa(rawPaket), satuanWaktuSewa: getPaketSatuanWaktuSewa(rawPaket),
foto_paket: getPaketMainPhoto(rawPaket), foto_paket: getPaketMainPhoto(rawPaket),
images: const <String>[], // Initialize with empty list, will be populated later images:
const <
String
>[], // Initialize with empty list, will be populated later
createdAt: DateTime.now(), createdAt: DateTime.now(),
updatedAt: DateTime.now(), updatedAt: DateTime.now(),
); );
@ -352,7 +358,7 @@ class OrderSewaPaketController extends GetxController {
if (items.isNotEmpty) { if (items.isNotEmpty) {
paketItems.value = items; paketItems.value = items;
debugPrint('✅ Loaded ${paketItems.length} package items'); debugPrint('✅ Loaded ${paketItems.length} package items');
} }
} catch (e) { } catch (e) {
debugPrint('❌ Error loading paket items: $e'); debugPrint('❌ Error loading paket items: $e');
paketItems.value = []; paketItems.value = [];
@ -1030,7 +1036,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Waktu Tidak Tersedia', 'Waktu Tidak Tersedia',
'Jam ini tidak tersedia karena ada aset dalam paket yang sudah dipesan', 'Jam ini tidak tersedia karena ada aset dalam paket yang sudah dipesan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1114,7 +1120,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Data paket tidak lengkap', 'Data paket tidak lengkap',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1131,7 +1137,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Silakan pilih waktu sewa', 'Silakan pilih waktu sewa',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1163,7 +1169,7 @@ class OrderSewaPaketController extends GetxController {
durasi: orderData['durasi'] ?? 1, // Default to 1 if not provided durasi: orderData['durasi'] ?? 1, // Default to 1 if not provided
totalHarga: orderData['total_harga'].toInt(), totalHarga: orderData['total_harga'].toInt(),
); );
// Create a mock result for navigation // Create a mock result for navigation
final resultData = { final resultData = {
'id': 'order_${DateTime.now().millisecondsSinceEpoch}', 'id': 'order_${DateTime.now().millisecondsSinceEpoch}',
@ -1174,21 +1180,23 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pesanan berhasil dibuat', 'Pesanan berhasil dibuat',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
// Navigate to payment page // Navigate to payment page
if (result && resultData != null && resultData['id'] != null) { if (result && resultData != null && resultData['id'] != null) {
navigationService.navigateToPembayaranSewa(resultData['id'].toString()); navigationService.navigateToPembayaranSewa(
resultData['id'].toString(),
);
} else if (result) { } else if (result) {
// If result is true but we don't have an ID, navigate back to sewa aset // If result is true but we don't have an ID, navigate back to sewa aset
navigationService.navigateToSewaAset(); navigationService.navigateToSewaAset();
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mendapatkan ID pesanan', 'Gagal mendapatkan ID pesanan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1196,7 +1204,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal membuat pesanan', 'Gagal membuat pesanan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1205,7 +1213,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal membuat pesanan', 'Gagal membuat pesanan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1215,7 +1223,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan: $e', 'Terjadi kesalahan: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1392,7 +1400,7 @@ class OrderSewaPaketController extends GetxController {
Get.snackbar( Get.snackbar(
'Peringatan', 'Peringatan',
'Pilih tanggal terlebih dahulu', 'Pilih tanggal terlebih dahulu',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange, backgroundColor: Colors.orange,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -378,7 +378,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Pesanan Dibatalkan', 'Pesanan Dibatalkan',
'Batas waktu pembayaran telah berakhir', 'Batas waktu pembayaran telah berakhir',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
duration: Duration(seconds: 5), duration: Duration(seconds: 5),
@ -417,7 +417,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengambil foto: ${e.toString()}', 'Gagal mengambil foto: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -443,7 +443,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memilih foto dari galeri: ${e.toString()}', 'Gagal memilih foto dari galeri: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -459,7 +459,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Mohon unggah bukti pembayaran terlebih dahulu', 'Mohon unggah bukti pembayaran terlebih dahulu',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -541,7 +541,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Bukti pembayaran berhasil diunggah', 'Bukti pembayaran berhasil diunggah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -550,7 +550,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengunggah bukti pembayaran: ${e.toString()}', 'Gagal mengunggah bukti pembayaran: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -654,7 +654,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pembayaran tunai berhasil disubmit', 'Pembayaran tunai berhasil disubmit',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1090,7 +1090,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Data berhasil diperbarui', 'Data berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
@ -1104,7 +1104,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memperbarui data', 'Gagal memperbarui data',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -305,6 +305,9 @@ class PembayaranSewaController extends GetxController
: 0, : 0,
'status': rentalData['status'] ?? 'MENUNGGU PEMBAYARAN', 'status': rentalData['status'] ?? 'MENUNGGU PEMBAYARAN',
'created_at': DateTime.now().toString(), 'created_at': DateTime.now().toString(),
'updated_at':
DateTime.now()
.toString(), // Explicitly set updated_at for countdown
'denda': 0, // Default value 'denda': 0, // Default value
'keterangan': '', // Default value 'keterangan': '', // Default value
'image_url': rentalData['imageUrl'], 'image_url': rentalData['imageUrl'],
@ -359,13 +362,19 @@ class PembayaranSewaController extends GetxController
'price_per_unit': 10000, 'price_per_unit': 10000,
'total_price': 50000, 'total_price': 50000,
'status': 'MENUNGGU PEMBAYARAN', 'status': 'MENUNGGU PEMBAYARAN',
'created_at': 'created_at': DateTime.now().toString(),
DateTime.now().toString(), // Use this for countdown calculation 'updated_at':
DateTime.now()
.toString(), // Explicitly set updated_at for countdown
'denda': 20000, // Dummy data for denda 'denda': 20000, // Dummy data for denda
'keterangan': 'keterangan':
'Terjadi kerusakan pada bagian kaki', // Dummy keterangan for denda 'Terjadi kerusakan pada bagian kaki', // Dummy keterangan for denda
}; };
debugPrint(
'DEBUG: Set updated_at in orderDetails: ${orderDetails.value['updated_at']}',
);
// Update the current step based on the status // Update the current step based on the status
updateCurrentStepBasedOnStatus(); updateCurrentStepBasedOnStatus();
@ -433,14 +442,27 @@ class PembayaranSewaController extends GetxController
if (data['status'] != null && if (data['status'] != null &&
data['status'].toString().isNotEmpty) { data['status'].toString().isNotEmpty) {
val?['status'] = data['status']; val?['status'] = data['status'];
debugPrint( debugPrint('📊 Order status from sewa_aset: ${data['status']}');
'📊 Order status from sewa_aset: \\${data['status']}',
);
} }
// Tambahkan mapping updated_at
// Ensure updated_at is always set
if (data['updated_at'] != null) { if (data['updated_at'] != null) {
val?['updated_at'] = data['updated_at']; val?['updated_at'] = data['updated_at'];
debugPrint(
'📅 Using updated_at from database: ${data['updated_at']}',
);
} else if (data['created_at'] != null) {
val?['updated_at'] = data['created_at'];
debugPrint(
'📅 Using created_at as fallback for updated_at: ${data['created_at']}',
);
} else {
val?['updated_at'] = DateTime.now().toIso8601String();
debugPrint(
'📅 Using current timestamp as fallback for updated_at',
);
} }
// Format rental period // Format rental period
if (data['waktu_mulai'] != null && if (data['waktu_mulai'] != null &&
data['waktu_selesai'] != null) { data['waktu_selesai'] != null) {
@ -448,12 +470,12 @@ class PembayaranSewaController extends GetxController
final startTime = DateTime.parse(data['waktu_mulai']); final startTime = DateTime.parse(data['waktu_mulai']);
final endTime = DateTime.parse(data['waktu_selesai']); final endTime = DateTime.parse(data['waktu_selesai']);
val?['rental_period'] = val?['rental_period'] =
'\\${startTime.day}/\\${startTime.month}/\\${startTime.year}, \\${startTime.hour}:\\${startTime.minute.toString().padLeft(2, '0')} - \\${endTime.hour}:\\${endTime.minute.toString().padLeft(2, '0')}'; '${startTime.day}/${startTime.month}/${startTime.year}, ${startTime.hour}:${startTime.minute.toString().padLeft(2, '0')} - ${endTime.hour}:${endTime.minute.toString().padLeft(2, '0')}';
debugPrint( debugPrint(
'✅ Successfully formatted rental period: \\${val?['rental_period']}', '✅ Successfully formatted rental period: ${val?['rental_period']}',
); );
} catch (e) { } catch (e) {
debugPrint('❌ Error parsing date: \\${e}'); debugPrint('❌ Error parsing date: ${e}');
} }
} else { } else {
debugPrint( debugPrint(
@ -577,7 +599,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Pesanan Dibatalkan', 'Pesanan Dibatalkan',
'Batas waktu pembayaran telah berakhir', 'Batas waktu pembayaran telah berakhir',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
duration: Duration(seconds: 5), duration: Duration(seconds: 5),
@ -624,7 +646,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengambil foto: \\${e.toString()}', 'Gagal mengambil foto: \\${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -653,7 +675,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memilih foto dari galeri: \\${e.toString()}', 'Gagal memilih foto dari galeri: \\${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -679,7 +701,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Mohon unggah bukti pembayaran terlebih dahulu', 'Mohon unggah bukti pembayaran terlebih dahulu',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -691,7 +713,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Tidak ada perubahan yang perlu disimpan', 'Tidak ada perubahan yang perlu disimpan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.blue, backgroundColor: Colors.blue,
colorText: Colors.white, colorText: Colors.white,
); );
@ -843,7 +865,7 @@ class PembayaranSewaController extends GetxController
val?['status'] == 'PEMBAYARAN DENDA') { val?['status'] == 'PEMBAYARAN DENDA') {
val?['status'] = 'PERIKSA PEMBAYARAN DENDA'; val?['status'] = 'PERIKSA PEMBAYARAN DENDA';
} else { } else {
val?['status'] = 'MEMERIKSA PEMBAYARAN'; val?['status'] = 'PERIKSA PEMBAYARAN';
} }
}); });
@ -894,7 +916,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Bukti pembayaran berhasil diunggah', 'Bukti pembayaran berhasil diunggah',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -903,7 +925,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal mengunggah bukti pembayaran: ${e.toString()}', 'Gagal mengunggah bukti pembayaran: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -930,19 +952,19 @@ class PembayaranSewaController extends GetxController
newStatus = 'MENUNGGU PEMBAYARAN'; newStatus = 'MENUNGGU PEMBAYARAN';
break; break;
case 1: case 1:
newStatus = 'MEMERIKSA PEMBAYARAN'; newStatus = 'PERIKSA PEMBAYARAN';
break; break;
case 2: case 2:
newStatus = 'DITERIMA'; newStatus = 'DITERIMA';
break; break;
case 3: case 3:
newStatus = 'PENGEMBALIAN'; newStatus = 'DIKEMBALIKAN';
break; break;
case 4: case 4:
newStatus = 'PEMBAYARAN DENDA'; newStatus = 'PEMBAYARAN DENDA';
break; break;
case 5: case 5:
newStatus = 'MEMERIKSA PEMBAYARAN DENDA'; newStatus = 'PERIKSA PEMBAYARAN DENDA';
break; break;
case 6: case 6:
newStatus = 'SELESAI'; newStatus = 'SELESAI';
@ -965,7 +987,7 @@ class PembayaranSewaController extends GetxController
case 'MENUNGGU PEMBAYARAN': case 'MENUNGGU PEMBAYARAN':
currentStep.value = 0; currentStep.value = 0;
break; break;
case 'MEMERIKSA PEMBAYARAN': case 'PERIKSA PEMBAYARAN':
currentStep.value = 1; currentStep.value = 1;
break; break;
case 'DITERIMA': case 'DITERIMA':
@ -974,7 +996,7 @@ class PembayaranSewaController extends GetxController
case 'AKTIF': case 'AKTIF':
currentStep.value = 3; currentStep.value = 3;
break; break;
case 'PENGEMBALIAN': case 'DIKEMBALIKAN':
currentStep.value = 4; currentStep.value = 4;
break; break;
case 'PEMBAYARAN DENDA': case 'PEMBAYARAN DENDA':
@ -1003,7 +1025,7 @@ class PembayaranSewaController extends GetxController
void submitCashPayment() { void submitCashPayment() {
// Update order status // Update order status
orderDetails.update((val) { orderDetails.update((val) {
val?['status'] = 'MEMERIKSA PEMBAYARAN'; val?['status'] = 'PERIKSA PEMBAYARAN';
}); });
// Cancel countdown timer as payment has been submitted // Cancel countdown timer as payment has been submitted
@ -1013,7 +1035,7 @@ class PembayaranSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pembayaran tunai berhasil disubmit', 'Pembayaran tunai berhasil disubmit',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1266,7 +1288,20 @@ class PembayaranSewaController extends GetxController
updateCurrentStepBasedOnStatus(); updateCurrentStepBasedOnStatus();
// Restart countdown timer if needed // Restart countdown timer if needed
if (orderDetails.value['status'] == 'MENUNGGU PEMBAYARAN') { if ((orderDetails.value['status'] ?? '').toString().toUpperCase() ==
'MENUNGGU PEMBAYARAN') {
debugPrint('Status is MENUNGGU PEMBAYARAN, restarting countdown timer');
// Ensure updated_at is set to current time if refreshing with MENUNGGU PEMBAYARAN status
if (orderDetails.value['updated_at'] == null) {
orderDetails.update((val) {
val?['updated_at'] = DateTime.now().toIso8601String();
});
debugPrint(
'Set updated_at to current time: ${orderDetails.value['updated_at']}',
);
}
_countdownTimer?.cancel(); _countdownTimer?.cancel();
startCountdownTimer(); startCountdownTimer();
} }

View File

@ -168,7 +168,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memuat data aset', 'Terjadi kesalahan saat memuat data aset',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -279,7 +279,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
message, message,
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -321,7 +321,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pesanan berhasil dibuat', 'Pesanan berhasil dibuat',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -383,7 +383,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Terjadi kesalahan saat memuat data paket', 'Terjadi kesalahan saat memuat data paket',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -429,7 +429,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal memuat data paket. Silakan coba lagi nanti.', 'Gagal memuat data paket. Silakan coba lagi nanti.',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -470,7 +470,7 @@ class SewaAsetController extends GetxController
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Pesanan paket berhasil dibuat', 'Pesanan paket berhasil dibuat',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -46,30 +46,51 @@ class WargaDashboardController extends GetxController {
final dendaAktifCount = 0.obs; final dendaAktifCount = 0.obs;
@override @override
void onInit() { void onInit() async {
super.onInit(); super.onInit();
// Set navigation index to Home (0) // Set navigation index to Home (0)
navigationService.setNavIndex(0); navigationService.setNavIndex(0);
// Load user data // Check if navigation is coming from login
fetchProfileFromWargaDesa(); final args = Get.arguments;
_loadUserData(); final bool isFromLogin = args != null && args['from_login'] == true;
// Load sample data if (isFromLogin) {
_loadSampleData(); print('onInit: Navigation from login detected, prioritizing data fetch');
}
// Load dummy data for bills and penalties // Verifikasi bahwa pengguna sudah login sebelum melakukan fetch data
loadDummyData(); if (_authProvider.currentUser != null) {
// Prioritize loading user profile data first
await fetchProfileFromWargaDesa();
// Load unpaid rentals // If the profile data was not loaded successfully, try again after a short delay
loadUnpaidRentals(); if (userName.value == 'Pengguna Warga' || userNik.value.isEmpty) {
print('onInit: Profile data not loaded, retrying after delay');
await Future.delayed(const Duration(milliseconds: 800));
await fetchProfileFromWargaDesa();
}
// Debug count sewa_aset by status // Load other user data
_debugCountSewaAset(); await _loadUserData();
// Load sewa aktif // Load other data in parallel to speed up the dashboard initialization
loadActiveRentals(); Future.wait([
_loadSampleData(),
loadDummyData(),
loadUnpaidRentals(),
_debugCountSewaAset(),
loadActiveRentals(),
]).then((_) => print('onInit: All data loaded successfully'));
// If coming from login, make sure UI is updated
if (isFromLogin) {
update();
}
} else {
print('onInit: User not logged in, skipping data fetch');
}
} }
Future<void> _loadUserData() async { Future<void> _loadUserData() async {
@ -116,7 +137,7 @@ class WargaDashboardController extends GetxController {
} }
} }
void _loadSampleData() { Future<void> _loadSampleData() async {
// Clear any existing data // Clear any existing data
activeRentals.clear(); activeRentals.clear();
@ -147,10 +168,37 @@ class WargaDashboardController extends GetxController {
navigationService.toSewaAset(); navigationService.toSewaAset();
} }
void refreshData() { Future<void> refreshData() async {
fetchProfileFromWargaDesa(); print('refreshData: Refreshing dashboard data');
_loadSampleData(); try {
loadDummyData(); // First fetch profile data
await fetchProfileFromWargaDesa();
await _loadUserData();
// Then load all other data in parallel
await Future.wait([
_loadSampleData(),
loadDummyData(),
loadUnpaidRentals(),
loadActiveRentals(),
_debugCountSewaAset(),
]);
// Update UI
update();
print('refreshData: Dashboard data refreshed successfully');
} catch (e) {
print('refreshData: Error refreshing data: $e');
// Show error message to user
Get.snackbar(
'Perhatian',
'Terjadi kesalahan saat memuat data',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.shade100,
colorText: Colors.red.shade900,
duration: const Duration(seconds: 3),
);
}
} }
void onNavItemTapped(int index) { void onNavItemTapped(int index) {
@ -176,12 +224,14 @@ class WargaDashboardController extends GetxController {
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 3}); Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 3});
} }
void logout() async { Future<void> logout() async {
print('logout: Logging out user');
await _authProvider.signOut(); await _authProvider.signOut();
navigationService.toLogin(); navigationService.toLogin();
print('logout: User logged out and redirected to login screen');
} }
void loadDummyData() { Future<void> loadDummyData() async {
// Dummy active bills // Dummy active bills
activeBills.clear(); activeBills.clear();
activeBills.add({ activeBills.add({
@ -331,24 +381,77 @@ class WargaDashboardController extends GetxController {
Future<void> fetchProfileFromWargaDesa() async { Future<void> fetchProfileFromWargaDesa() async {
try { try {
final user = _authProvider.currentUser; final user = _authProvider.currentUser;
if (user == null) return; if (user == null) {
print(
'fetchProfileFromWargaDesa: No current user found, skipping fetch',
);
return; // Exit early if no user is logged in
}
final userId = user.id; final userId = user.id;
print('fetchProfileFromWargaDesa: Fetching data for user: $userId');
final data = final data =
await _authProvider.client await _authProvider.client
.from('warga_desa') .from('warga_desa')
.select('nik, alamat, email, nama_lengkap, no_hp, avatar') .select('nik, alamat, email, nama_lengkap, no_hp, avatar')
.eq('user_id', userId) .eq('user_id', userId)
.maybeSingle(); .maybeSingle();
if (data != null) { if (data != null) {
print('fetchProfileFromWargaDesa: Data retrieved successfully');
userNik.value = data['nik']?.toString() ?? ''; userNik.value = data['nik']?.toString() ?? '';
userAddress.value = data['alamat']?.toString() ?? ''; userAddress.value = data['alamat']?.toString() ?? '';
userEmail.value = data['email']?.toString() ?? ''; userEmail.value = data['email']?.toString() ?? '';
userName.value = data['nama_lengkap']?.toString() ?? ''; userName.value = data['nama_lengkap']?.toString() ?? '';
userPhone.value = data['no_hp']?.toString() ?? ''; userPhone.value = data['no_hp']?.toString() ?? '';
userAvatar.value = data['avatar']?.toString() ?? ''; userAvatar.value = data['avatar']?.toString() ?? '';
// Trigger UI refresh
update();
print('fetchProfileFromWargaDesa: Profile data updated');
} else {
print('fetchProfileFromWargaDesa: No data found for user: $userId');
} }
} catch (e) { } catch (e) {
print('Error fetching profile from warga_desa: $e'); print('Error fetching profile from warga_desa: $e');
// If it fails, try again after a delay
await Future.delayed(const Duration(seconds: 1));
try {
await _retryFetchProfile();
} catch (retryError) {
print('Retry error fetching profile: $retryError');
}
}
}
// Helper method to retry fetching profile
Future<void> _retryFetchProfile() async {
final user = _authProvider.currentUser;
if (user == null) {
print('_retryFetchProfile: No current user found, skipping retry');
return; // Exit early if no user is logged in
}
print('_retryFetchProfile: Retrying fetch for user: ${user.id}');
final data =
await _authProvider.client
.from('warga_desa')
.select('nik, alamat, email, nama_lengkap, no_hp, avatar')
.eq('user_id', user.id)
.maybeSingle();
if (data != null) {
print('_retryFetchProfile: Data retrieved successfully on retry');
userNik.value = data['nik']?.toString() ?? '';
userAddress.value = data['alamat']?.toString() ?? '';
userEmail.value = data['email']?.toString() ?? '';
userName.value = data['nama_lengkap']?.toString() ?? '';
userPhone.value = data['no_hp']?.toString() ?? '';
userAvatar.value = data['avatar']?.toString() ?? '';
update();
print('_retryFetchProfile: Profile data updated');
} }
} }
@ -533,7 +636,7 @@ class WargaDashboardController extends GetxController {
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Tidak dapat mengakses ${source == ImageSource.camera ? 'kamera' : 'galeri'}', 'Tidak dapat mengakses ${source == ImageSource.camera ? 'kamera' : 'galeri'}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red.shade700, backgroundColor: Colors.red.shade700,
colorText: Colors.white, colorText: Colors.white,
duration: const Duration(seconds: 3), duration: const Duration(seconds: 3),

View File

@ -254,10 +254,28 @@ class WargaSewaController extends GetxController
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records'); debugPrint('Fetched ${sewaAsetList.length} sewa_aset records');
// Debug the structure of the first record if available
if (sewaAsetList.isNotEmpty) {
debugPrint('Sample sewa_aset record: ${sewaAsetList.first}');
debugPrint('updated_at field: ${sewaAsetList.first['updated_at']}');
}
// Process each sewa_aset record // Process each sewa_aset record
for (var sewaAset in sewaAsetList) { for (var sewaAset in sewaAsetList) {
final processedData = await _processRentalData(sewaAset); final processedData = await _processRentalData(sewaAset);
processedData['status'] = sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN'; processedData['status'] = sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN';
// Ensure updated_at is set correctly
if (sewaAset['updated_at'] == null &&
processedData['status'] == 'MENUNGGU PEMBAYARAN') {
// If updated_at is null but status is MENUNGGU PEMBAYARAN, use created_at as fallback
processedData['updated_at'] =
sewaAset['created_at'] ?? DateTime.now().toIso8601String();
debugPrint(
'Using created_at as fallback for updated_at: ${processedData['updated_at']}',
);
}
rentals.add(processedData); rentals.add(processedData);
} }
@ -321,7 +339,7 @@ class WargaSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Berhasil', 'Berhasil',
'Pesanan berhasil dibatalkan', 'Pesanan berhasil dibatalkan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -337,7 +355,7 @@ class WargaSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Gagal membatalkan pesanan: $e', 'Gagal membatalkan pesanan: $e',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -377,7 +395,7 @@ class WargaSewaController extends GetxController
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Navigasi ke halaman pembayaran', 'Navigasi ke halaman pembayaran',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
} }

View File

@ -285,9 +285,10 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
return WillPopScope( return WillPopScope(
onWillPop: () async { onWillPop: () async {
// Handle back button press here // Bersihkan controller saat pengguna keluar dari halaman dengan tombol back perangkat
handleBackButtonPress(); controller.clearData();
return false; // We handle the navigation ourselves debugPrint('🧹 Controller data cleared by WillPopScope');
return true;
}, },
child: Scaffold( child: Scaffold(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
@ -474,108 +475,60 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
}, },
// Add onTap handler for the image // Add onTap handler for the image
onTap: () { onTap: () {
debugPrint("📸 Image tapped - opening fullscreen viewer"); debugPrint(
final imageUrl = controller.getCurrentPhotoUrl(); "📸 Image tapped - attempting to open fullscreen viewer",
if (imageUrl != null && imageUrl.isNotEmpty) { );
debugPrint("📸 Current image URL: $imageUrl"); try {
debugPrint("📸 Total photos: ${controller.assetPhotos.length}"); final imageUrl = controller.getCurrentPhotoUrl();
if (imageUrl != null && imageUrl.isNotEmpty) {
debugPrint("📸 Current image URL: $imageUrl");
debugPrint(
"📸 Total photos: ${controller.assetPhotos.length}",
);
// Extract all image URLs from the assetPhotos collection // Extract all image URLs from the assetPhotos collection
final List<String> photoUrls = []; final List<String> photoUrls = [];
for (var photo in controller.assetPhotos) { for (var photo in controller.assetPhotos) {
final url = photo.fotoAset; final url = photo.fotoAset;
if (url.isNotEmpty) { if (url.isNotEmpty) {
photoUrls.add(url); photoUrls.add(url);
debugPrint("📸 Added photo URL: $url"); debugPrint("📸 Added photo URL: $url");
}
} }
if (photoUrls.isEmpty) {
debugPrint("📸 No valid photo URLs found");
return;
}
debugPrint(
"📸 About to navigate to fullscreen viewer with ${photoUrls.length} photos",
);
// Navigate to full screen image viewer using GetX
Get.to(
() => FullScreenImageViewer(
imageUrls: photoUrls,
initialIndex: controller.currentPhotoIndex.value,
),
fullscreenDialog: true,
transition: Transition.fade,
);
debugPrint("📸 Navigation initiated");
} else {
debugPrint("📸 No valid current image URL");
} }
} catch (e) {
if (photoUrls.isEmpty) { debugPrint("📸 ERROR in image tap handler: $e");
debugPrint("📸 No valid photo URLs found"); Get.snackbar(
return; 'Error',
} 'Failed to open image viewer',
snackPosition: SnackPosition.BOTTOM,
showDialog( backgroundColor: Colors.red.shade100,
context: Get.context!, colorText: Colors.red.shade900,
builder: duration: const Duration(seconds: 2),
(context) => Dialog(
insetPadding: EdgeInsets.zero,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black,
child: Stack(
children: [
// Image
Center(
child: InteractiveViewer(
panEnabled: true,
boundaryMargin: EdgeInsets.all(80),
minScale: 0.5,
maxScale: 4,
child: CachedNetworkImage(
imageUrl:
photoUrls[controller
.currentPhotoIndex
.value],
fit: BoxFit.contain,
placeholder:
(context, url) => Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
errorWidget:
(context, url, error) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.broken_image_rounded,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
'Gagal memuat foto',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
),
),
),
),
// Close button
Positioned(
top: 40,
right: 20,
child: IconButton(
icon: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: Icon(
Icons.close,
color: Colors.white,
),
),
onPressed: () => Navigator.of(context).pop(),
),
),
],
),
),
),
); );
} else {
debugPrint("📸 No valid current image URL");
} }
}, },
child: ClipRRect( child: ClipRRect(
@ -667,17 +620,19 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
// Gradient overlay // Gradient overlay
Positioned.fill( Positioned.fill(
child: DecoratedBox( child: IgnorePointer(
decoration: BoxDecoration( child: DecoratedBox(
gradient: LinearGradient( decoration: BoxDecoration(
begin: Alignment.topCenter, gradient: LinearGradient(
end: Alignment.bottomCenter, begin: Alignment.topCenter,
colors: [ end: Alignment.bottomCenter,
Colors.black.withOpacity(0.1), colors: [
Colors.transparent, Colors.black.withOpacity(0.1),
Colors.black.withOpacity(0.3), Colors.transparent,
], Colors.black.withOpacity(0.3),
stops: [0.0, 0.6, 1.0], ],
stops: [0.0, 0.6, 1.0],
),
), ),
), ),
), ),
@ -687,13 +642,15 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
Positioned( Positioned(
bottom: 16, bottom: 16,
right: 16, right: 16,
child: Container( child: IgnorePointer(
padding: EdgeInsets.all(8), child: Container(
decoration: BoxDecoration( padding: EdgeInsets.all(8),
color: Colors.black.withOpacity(0.5), decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.zoom_in, color: Colors.white, size: 24),
), ),
child: Icon(Icons.zoom_in, color: Colors.white, size: 24),
), ),
), ),
@ -1058,88 +1015,176 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
asetId, asetId,
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
return Container( return GestureDetector(
width: 60, onTap: () async {
height: 60, debugPrint(
decoration: BoxDecoration( "📸 Package item image tapped - attempting to open fullscreen viewer for asset ID: $asetId",
borderRadius: );
BorderRadius.circular( try {
8, // Get all photos for this asset
), final photos =
color: await controller
AppColors .asetProvider
.surfaceLight, .getAsetPhotos(
), asetId,
child: );
snapshot.connectionState == if (photos.isEmpty) {
ConnectionState debugPrint(
.waiting "📸 No photos found for asset ID: $asetId",
? Center( );
child: SizedBox( return;
width: 20, }
height: 20,
child: CircularProgressIndicator( // Extract valid photo URLs
strokeWidth: final List<String>
2, photoUrls = [];
color: for (var photo
AppColors in photos) {
.primary, final url =
), photo.fotoAset;
if (url.isNotEmpty) {
photoUrls.add(url);
debugPrint(
"📸 Added photo URL: $url",
);
}
}
if (photoUrls.isEmpty) {
debugPrint(
"📸 No valid photo URLs found",
);
return;
}
debugPrint(
"📸 About to navigate to fullscreen viewer with ${photoUrls.length} photos",
);
// Navigate to full screen image viewer using GetX
Get.to(
() =>
FullScreenImageViewer(
imageUrls:
photoUrls,
initialIndex: 0,
), ),
) fullscreenDialog:
: snapshot.data != true,
null transition:
? ClipRRect( Transition.fade,
borderRadius: );
BorderRadius.circular(
8, debugPrint(
"📸 Navigation initiated",
);
} catch (e) {
debugPrint(
"📸 ERROR in package item image tap handler: $e",
);
Get.snackbar(
'Error',
'Failed to open image viewer',
snackPosition:
SnackPosition
.BOTTOM,
backgroundColor:
Colors
.red
.shade100,
colorText:
Colors
.red
.shade900,
duration:
const Duration(
seconds: 2,
),
);
}
},
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(
8,
),
color:
AppColors
.surfaceLight,
),
child:
snapshot.connectionState ==
ConnectionState
.waiting
? Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth:
2,
color:
AppColors
.primary,
), ),
child: CachedNetworkImage( ),
imageUrl: )
snapshot : snapshot.data !=
.data!, null
fit: ? ClipRRect(
BoxFit borderRadius:
.cover, BorderRadius.circular(
placeholder: 8,
( ),
context, child: CachedNetworkImage(
url, imageUrl:
) => Center( snapshot
child: SizedBox( .data!,
width: fit:
20, BoxFit
height: .cover,
20, placeholder:
child: CircularProgressIndicator( (
strokeWidth: context,
2, url,
color: ) => Center(
AppColors.primary, child: SizedBox(
width:
20,
height:
20,
child: CircularProgressIndicator(
strokeWidth:
2,
color:
AppColors.primary,
),
), ),
), ),
), errorWidget:
errorWidget: (
( context,
context, url,
url, error,
error, ) => Icon(
) => Icon( Icons
Icons .image_not_supported_outlined,
.image_not_supported_outlined, color:
color: AppColors.textLight,
AppColors ),
.textLight, ),
), )
: Icon(
Icons
.image_not_supported_outlined,
color:
AppColors
.textLight,
), ),
) ),
: Icon(
Icons
.image_not_supported_outlined,
color:
AppColors
.textLight,
),
); );
}, },
), ),
@ -2458,98 +2503,173 @@ class _FullScreenImageViewerState extends State<FullScreenImageViewer> {
for (int i = 0; i < widget.imageUrls.length && i < 3; i++) { for (int i = 0; i < widget.imageUrls.length && i < 3; i++) {
debugPrint("📸 Image URL $i: ${widget.imageUrls[i]}"); debugPrint("📸 Image URL $i: ${widget.imageUrls[i]}");
} }
// Hide status bar for immersive view
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [], // Hide all system UI
).catchError((error) {
debugPrint("📸 Error setting system UI mode: $error");
});
} }
@override @override
void dispose() { void dispose() {
// Restore status bar when exiting
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values, // Restore all system UI
).catchError((error) {
debugPrint("📸 Error restoring system UI mode: $error");
});
pageController.dispose(); pageController.dispose();
super.dispose(); super.dispose();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return WillPopScope(
backgroundColor: Colors.black, onWillPop: () async {
body: Stack( // Ensure UI is restored on back button press
children: [ SystemChrome.setEnabledSystemUIMode(
// Photo Gallery SystemUiMode.manual,
PhotoViewGallery.builder( overlays: SystemUiOverlay.values,
scrollPhysics: const BouncingScrollPhysics(), );
builder: (BuildContext context, int index) { return true;
final imageUrl = widget.imageUrls[index]; },
debugPrint("📸 Building image at index $index: $imageUrl"); child: Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
// Photo Gallery
GestureDetector(
onTap: () {
// Add tap functionality to close when tapping on image
Navigator.of(context).pop();
},
child: PhotoViewGallery.builder(
scrollPhysics: const BouncingScrollPhysics(),
builder: (BuildContext context, int index) {
final imageUrl = widget.imageUrls[index];
debugPrint("📸 Building image at index $index: $imageUrl");
return PhotoViewGalleryPageOptions( return PhotoViewGalleryPageOptions(
imageProvider: CachedNetworkImageProvider(imageUrl), imageProvider: CachedNetworkImageProvider(imageUrl),
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained * 0.8, minScale: PhotoViewComputedScale.contained * 0.8,
maxScale: PhotoViewComputedScale.covered * 2.0, maxScale: PhotoViewComputedScale.covered * 2.0,
heroAttributes: PhotoViewHeroAttributes(tag: "photo_$index"), heroAttributes: PhotoViewHeroAttributes(
); tag: "photo_$index",
}, ),
itemCount: widget.imageUrls.length, errorBuilder: (context, error, stackTrace) {
loadingBuilder: debugPrint("📸 Error loading image: $error");
(context, event) => Center( return Container(
child: SizedBox( color: Colors.black,
width: 30.0, child: Center(
height: 30.0, child: Column(
child: CircularProgressIndicator( mainAxisSize: MainAxisSize.min,
value: children: [
event == null Icon(
? 0 Icons.broken_image_rounded,
: event.cumulativeBytesLoaded / color: Colors.white70,
event.expectedTotalBytes!, size: 50,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white), ),
const SizedBox(height: 16),
const Text(
'Gagal memuat gambar',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
],
),
),
);
},
);
},
itemCount: widget.imageUrls.length,
loadingBuilder:
(context, event) => Center(
child: Container(
width: 40.0,
height: 40.0,
child: CircularProgressIndicator(
value:
event == null
? 0
: event.cumulativeBytesLoaded /
(event.expectedTotalBytes ?? 1),
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
),
backgroundDecoration: const BoxDecoration(
color: Colors.black,
),
pageController: pageController,
onPageChanged: (index) {
setState(() {
currentIndex = index;
debugPrint("📸 Page changed to index: $index");
});
},
),
),
// Close button
Positioned(
top: 20,
left: 20,
child: GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 24,
), ),
), ),
), ),
backgroundDecoration: BoxDecoration(color: Colors.black),
pageController: pageController,
onPageChanged: (index) {
setState(() {
currentIndex = index;
debugPrint("📸 Page changed to index: $index");
});
},
),
// Close button
Positioned(
top: 40,
left: 20,
child: IconButton(
icon: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: Icon(Icons.close, color: Colors.white),
), ),
onPressed: () => Navigator.of(context).pop(),
),
),
// Image counter // Image counter
Positioned( Positioned(
top: 40, top: 20,
right: 20, right: 20,
child: Container( child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(
decoration: BoxDecoration( horizontal: 12,
color: Colors.black54, vertical: 8,
borderRadius: BorderRadius.circular(16), ),
), decoration: BoxDecoration(
child: Text( color: Colors.black54,
'${currentIndex + 1}/${widget.imageUrls.length}', borderRadius: BorderRadius.circular(16),
style: TextStyle( ),
color: Colors.white, child: Text(
fontWeight: FontWeight.bold, '${currentIndex + 1}/${widget.imageUrls.length}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
), ),
), ),
), ],
), ),
], ),
), ),
); );
} }

View File

@ -318,7 +318,7 @@
Get.snackbar( Get.snackbar(
'Perhatian', 'Perhatian',
'Pilih jam mulai terlebih dahulu', 'Pilih jam mulai terlebih dahulu',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: AppColors.warning, backgroundColor: AppColors.warning,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -102,25 +102,25 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
.toUpperCase(); .toUpperCase();
final updatedAtStr = final updatedAtStr =
controller.orderDetails.value['updated_at']; controller.orderDetails.value['updated_at'];
print('DEBUG status: ' + status); debugPrint('DEBUG status (batas waktu): $status');
print( debugPrint(
'DEBUG updated_at (raw): ' + 'DEBUG updated_at batas waktu (raw): ${updatedAtStr?.toString() ?? 'NULL'}',
(updatedAtStr?.toString() ?? 'NULL'),
); );
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) { if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
try { try {
final updatedAt = DateTime.parse(updatedAtStr); final updatedAt = DateTime.parse(updatedAtStr);
print( debugPrint(
'DEBUG updated_at (parsed): ' + 'DEBUG updated_at batas waktu (parsed): ${updatedAt.toIso8601String()}',
updatedAt.toIso8601String(),
); );
return CountdownTimerWidget(updatedAt: updatedAt); return CountdownTimerWidget(updatedAt: updatedAt);
} catch (e) { } catch (e) {
print('ERROR parsing updated_at: ' + e.toString()); debugPrint('ERROR parsing updated_at batas waktu: $e');
return Text( // Fallback to current time if parsing fails
'Format tanggal salah', final now = DateTime.now();
style: TextStyle(color: Colors.red), debugPrint(
'Using current time as fallback for batas waktu: ${now.toIso8601String()}',
); );
return CountdownTimerWidget(updatedAt: now);
} }
} }
return SizedBox.shrink(); return SizedBox.shrink();
@ -322,25 +322,25 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
.toUpperCase(); .toUpperCase();
final updatedAtStr = final updatedAtStr =
controller.orderDetails.value['updated_at']; controller.orderDetails.value['updated_at'];
print('DEBUG status: ' + status); debugPrint('DEBUG status (batas waktu): $status');
print( debugPrint(
'DEBUG updated_at (raw): ' + 'DEBUG updated_at batas waktu (raw): ${updatedAtStr?.toString() ?? 'NULL'}',
(updatedAtStr?.toString() ?? 'NULL'),
); );
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) { if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
try { try {
final updatedAt = DateTime.parse(updatedAtStr); final updatedAt = DateTime.parse(updatedAtStr);
print( debugPrint(
'DEBUG updated_at (parsed): ' + 'DEBUG updated_at batas waktu (parsed): ${updatedAt.toIso8601String()}',
updatedAt.toIso8601String(),
); );
return CountdownTimerWidget(updatedAt: updatedAt); return CountdownTimerWidget(updatedAt: updatedAt);
} catch (e) { } catch (e) {
print('ERROR parsing updated_at: ' + e.toString()); debugPrint('ERROR parsing updated_at batas waktu: $e');
return Text( // Fallback to current time if parsing fails
'Format tanggal salah', final now = DateTime.now();
style: TextStyle(color: Colors.red), debugPrint(
'Using current time as fallback for batas waktu: ${now.toIso8601String()}',
); );
return CountdownTimerWidget(updatedAt: now);
} }
} }
return SizedBox.shrink(); return SizedBox.shrink();
@ -363,7 +363,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
'step': 0, 'step': 0,
}, },
{ {
'title': 'Memeriksa Pembayaran', 'title': 'Periksa Pembayaran',
'description': 'Pembayaran sedang diverifikasi oleh petugas', 'description': 'Pembayaran sedang diverifikasi oleh petugas',
'icon': Icons.receipt_long, 'icon': Icons.receipt_long,
'step': 1, 'step': 1,
@ -381,8 +381,8 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
'step': 3, 'step': 3,
}, },
{ {
'title': 'Pengembalian', 'title': 'Dikembalikan',
'description': 'Proses pengembalian aset sewa', 'description': 'Aset sudah dikembalikan',
'icon': Icons.assignment_return, 'icon': Icons.assignment_return,
'step': 4, 'step': 4,
}, },
@ -2585,9 +2585,8 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
print( debugPrint(
'DEBUG [CountdownTimerWidget] updatedAt: ' + 'DEBUG [CountdownTimerWidget] updatedAt: ${widget.updatedAt.toIso8601String()}',
widget.updatedAt.toIso8601String(),
); );
updateRemaining(); updateRemaining();
timer = Timer.periodic( timer = Timer.periodic(
@ -2599,18 +2598,29 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
void updateRemaining() { void updateRemaining() {
final now = DateTime.now(); final now = DateTime.now();
final end = widget.updatedAt.add(const Duration(hours: 1)); final end = widget.updatedAt.add(const Duration(hours: 1));
debugPrint('Current time: $now');
debugPrint('Deadline: $end');
setState(() { setState(() {
remaining = end.difference(now); remaining = end.difference(now);
debugPrint('Remaining time: ${remaining.inSeconds} seconds');
if (remaining.isNegative) { if (remaining.isNegative) {
debugPrint('Countdown expired, setting to zero');
remaining = Duration.zero; remaining = Duration.zero;
timer?.cancel(); timer?.cancel();
widget.onTimeout?.call(); if (widget.onTimeout != null) {
debugPrint('Calling onTimeout callback');
widget.onTimeout?.call();
}
} }
}); });
} }
@override @override
void dispose() { void dispose() {
debugPrint('CountdownTimerWidget disposed');
timer?.cancel(); timer?.cancel();
super.dispose(); super.dispose();
} }
@ -2620,9 +2630,15 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
if (remaining.inSeconds <= 0) { if (remaining.inSeconds <= 0) {
return Text('Waktu habis', style: TextStyle(color: Colors.red)); return Text('Waktu habis', style: TextStyle(color: Colors.red));
} }
final h = remaining.inHours; final h = remaining.inHours;
final m = remaining.inMinutes % 60; final m = remaining.inMinutes % 60;
final s = remaining.inSeconds % 60; final s = remaining.inSeconds % 60;
final timeString =
'${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
debugPrint('Rendering countdown: $timeString');
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -2636,7 +2652,7 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
Icon(Icons.timer_outlined, size: 14, color: Colors.red), Icon(Icons.timer_outlined, size: 14, color: Colors.red),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'Bayar dalam ${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}', 'Bayar dalam $timeString',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@ -1524,7 +1524,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID aset tidak valid', 'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1754,7 +1754,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID aset tidak valid', 'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -1801,7 +1801,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID aset tidak valid', 'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );

View File

@ -14,6 +14,19 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
// Check if coming from login and trigger refresh
WidgetsBinding.instance.addPostFrameCallback((_) {
final args = Get.arguments;
final bool isFromLogin = args != null && args['from_login'] == true;
if (isFromLogin) {
// Trigger refresh after UI is built
controller.refreshData();
print(
'WargaDashboardView: Auto-refreshed data due to login navigation',
);
}
});
return WillPopScope( return WillPopScope(
onWillPop: () async => false, // Prevent back navigation onWillPop: () async => false, // Prevent back navigation
child: WargaLayout( child: WargaLayout(

View File

@ -80,7 +80,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Terjadi kesalahan saat menyimpan foto profil', 'Terjadi kesalahan saat menyimpan foto profil',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -99,7 +99,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Terjadi kesalahan saat menghapus foto profil', 'Terjadi kesalahan saat menghapus foto profil',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -124,7 +124,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Perubahan berhasil disimpan', 'Perubahan berhasil disimpan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -132,7 +132,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar( Get.snackbar(
'Gagal', 'Gagal',
'Terjadi kesalahan saat menyimpan perubahan', 'Terjadi kesalahan saat menyimpan perubahan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -156,6 +156,9 @@ class WargaProfileView extends GetView<WargaDashboardController> {
: IconButton( : IconButton(
onPressed: () { onPressed: () {
isEditing.value = true; isEditing.value = true;
// Update controllers with current values when entering edit mode
nameController.text = controller.userName.value;
phoneController.text = controller.userPhone.value;
}, },
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.edit_outlined),
tooltip: 'Edit Profil', tooltip: 'Edit Profil',
@ -365,14 +368,6 @@ class WargaProfileView extends GetView<WargaDashboardController> {
} else { } else {
// Set avatar deleted flag // Set avatar deleted flag
isAvatarDeleted.value = true; isAvatarDeleted.value = true;
Get.snackbar(
'Info',
'Foto profil akan dihapus setelah menekan Simpan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange.shade700,
colorText: Colors.white,
);
} }
}, },
icon: const Icon(Icons.delete_outline, size: 16), icon: const Icon(Icons.delete_outline, size: 16),
@ -468,163 +463,242 @@ class WargaProfileView extends GetView<WargaDashboardController> {
) { ) {
return Column( return Column(
children: [ children: [
// Section 1: Data Diri // Section 1: Data Diri - Dengan desain yang lebih modern
Card( Card(
elevation: 4, elevation: 2,
shadowColor: AppColors.primary.withOpacity(0.3),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(20),
), ),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding( child: Container(
padding: const EdgeInsets.all(20), decoration: BoxDecoration(
child: Column( borderRadius: BorderRadius.circular(20),
crossAxisAlignment: CrossAxisAlignment.start, gradient: LinearGradient(
children: [ begin: Alignment.topLeft,
Row( end: Alignment.bottomRight,
children: [ colors: [Colors.white, AppColors.primary.withOpacity(0.05)],
Icon( ),
Icons.person_rounded, ),
color: AppColors.primary, child: Padding(
size: 22, padding: const EdgeInsets.all(20),
), child: Column(
const SizedBox(width: 10), crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'Data Diri', Row(
style: TextStyle( children: [
fontSize: 18, Container(
fontWeight: FontWeight.bold, padding: const EdgeInsets.all(10),
color: AppColors.primary, decoration: BoxDecoration(
), color: AppColors.primary.withOpacity(0.1),
), borderRadius: BorderRadius.circular(12),
if (isEditing) ...[ ),
const Spacer(), child: Icon(
Text( Icons.person_rounded,
'Mode Edit', color: AppColors.primary,
style: TextStyle( size: 22,
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.orange.shade700,
), ),
), ),
const SizedBox(width: 6), const SizedBox(width: 14),
Icon( Column(
Icons.edit_note, crossAxisAlignment: CrossAxisAlignment.start,
color: Colors.orange.shade700, children: [
size: 18, Text(
'Data Diri',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
Text(
'Informasi personal Anda',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
), ),
if (isEditing) ...[
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(
Icons.edit_note,
color: Colors.orange.shade700,
size: 16,
),
const SizedBox(width: 4),
Text(
'Mode Edit',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.orange.shade700,
),
),
],
),
),
],
], ],
], ),
), const SizedBox(height: 24),
const SizedBox(height: 20), // Email - always read-only
// Email - always read-only _buildInfoItemModern(
_buildInfoItemModern( context,
context, icon: Icons.email_rounded,
icon: Icons.email_rounded, title: 'Email',
title: 'Email', value: controller.userEmail.value,
value: controller.userEmail.value, isVerified: true,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Nama Lengkap - editable // Nama Lengkap - editable
_buildEditableInfoItem( _buildEditableInfoItem(
context, context,
icon: Icons.person_rounded, icon: Icons.person_rounded,
title: 'Nama Lengkap', title: 'Nama Lengkap',
value: controller.userName.value, value: controller.userName.value,
isEditing: isEditing, isEditing: isEditing,
controller: nameController, controller: nameController,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Nomor Telepon - editable // Nomor Telepon - editable
_buildEditableInfoItem( _buildEditableInfoItem(
context, context,
icon: Icons.phone_rounded, icon: Icons.phone_rounded,
title: 'Nomor Telepon', title: 'Nomor Telepon',
value: controller.userPhone.value, value: controller.userPhone.value,
isEditing: isEditing, isEditing: isEditing,
controller: phoneController, controller: phoneController,
keyboardType: TextInputType.phone, keyboardType: TextInputType.phone,
), ),
], ],
),
), ),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Section 2: Informasi Warga // Section 2: Informasi Warga - Dengan desain yang lebih modern
Card( Card(
elevation: 4, elevation: 2,
shadowColor: AppColors.accent.withOpacity(0.3),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(20),
), ),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding( child: Container(
padding: const EdgeInsets.all(20), decoration: BoxDecoration(
child: Column( borderRadius: BorderRadius.circular(20),
crossAxisAlignment: CrossAxisAlignment.start, gradient: LinearGradient(
children: [ begin: Alignment.topLeft,
Row( end: Alignment.bottomRight,
children: [ colors: [Colors.white, AppColors.accent.withOpacity(0.05)],
Icon( ),
Icons.badge_rounded, ),
color: AppColors.primary, child: Padding(
size: 22, padding: const EdgeInsets.all(20),
), child: Column(
const SizedBox(width: 10), crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
'Informasi Warga', Row(
style: TextStyle( children: [
fontSize: 18, Container(
fontWeight: FontWeight.bold, padding: const EdgeInsets.all(10),
color: AppColors.primary, decoration: BoxDecoration(
color: AppColors.accent.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.badge_rounded,
color: AppColors.accent,
size: 22,
),
), ),
const SizedBox(width: 14),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Informasi Warga',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.accent,
),
),
Text(
'Detail kependudukan',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
],
),
const SizedBox(height: 24),
_buildInfoItemModern(
context,
icon: Icons.credit_card_rounded,
title: 'NIK',
value: controller.userNik.value,
isImportant: true,
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.calendar_today_rounded,
title: 'Tanggal Lahir',
value: formatTanggalLahir(
controller.userTanggalLahir.value,
), ),
], ),
), const SizedBox(height: 16),
const SizedBox(height: 20), _buildInfoItemModern(
_buildInfoItemModern( context,
context, icon: Icons.home_rounded,
icon: Icons.credit_card_rounded, title: 'Alamat',
title: 'NIK', value: controller.userAddress.value,
value: controller.userNik.value, isMultiLine: true,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildInfoItemModern( // Tampilkan RT/RW dan Kelurahan/Desa dalam dua baris terpisah
context, _buildInfoItemModern(
icon: Icons.calendar_today_rounded, context,
title: 'Tanggal Lahir', icon: Icons.location_on_rounded,
value: controller.userTanggalLahir.value, title: 'RT/RW',
), value: controller.userRtRw.value,
const SizedBox(height: 16), ),
_buildInfoItemModern( const SizedBox(height: 16),
context, _buildInfoItemModern(
icon: Icons.home_rounded, context,
title: 'Alamat', icon: Icons.location_city_rounded,
value: controller.userAddress.value, title: 'Kelurahan/Desa',
isMultiLine: true, value: controller.userKelurahanDesa.value,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildInfoItemModern( _buildInfoItemModern(
context, context,
icon: Icons.location_on_rounded, icon: Icons.map_rounded,
title: 'RT/RW', title: 'Kecamatan',
value: controller.userRtRw.value, value: controller.userKecamatan.value,
), ),
const SizedBox(height: 16), ],
_buildInfoItemModern( ),
context,
icon: Icons.location_city_rounded,
title: 'Kelurahan/Desa',
value: controller.userKelurahanDesa.value,
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.map_rounded,
title: 'Kecamatan',
value: controller.userKecamatan.value,
),
],
), ),
), ),
), ),
@ -638,46 +712,85 @@ class WargaProfileView extends GetView<WargaDashboardController> {
required String title, required String title,
required String value, required String value,
bool isMultiLine = false, bool isMultiLine = false,
bool isImportant = false,
bool isVerified = false,
bool isCompact = false,
}) { }) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey.shade50, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200), border: Border.all(
color:
isImportant
? AppColors.primary.withOpacity(0.3)
: Colors.grey.shade200,
width: isImportant ? 1.5 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: isCompact ? 12 : 16,
), ),
padding: const EdgeInsets.all(16),
child: Row( child: Row(
crossAxisAlignment: crossAxisAlignment:
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center, isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [ children: [
Container( Container(
padding: const EdgeInsets.all(8), padding: EdgeInsets.all(isCompact ? 6 : 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1), color:
isImportant
? AppColors.primary.withOpacity(0.15)
: AppColors.accent.withOpacity(0.1),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Icon(icon, color: AppColors.primary, size: 20), child: Icon(
icon,
color: isImportant ? AppColors.primary : AppColors.accent,
size: isCompact ? 16 : 20,
),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Row(
title, children: [
style: TextStyle( Text(
fontSize: 13, title,
fontWeight: FontWeight.w500, style: TextStyle(
color: Colors.grey.shade600, fontSize: isCompact ? 12 : 13,
), fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
if (isVerified) ...[
const SizedBox(width: 4),
Icon(Icons.verified, size: 14, color: Colors.green),
],
],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
value.isEmpty ? 'Tidak tersedia' : value, value.isEmpty ? 'Tidak tersedia' : value,
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: isCompact ? 14 : 15,
fontWeight: FontWeight.w600, fontWeight: isImportant ? FontWeight.w700 : FontWeight.w600,
color: Colors.grey.shade800, color:
isImportant
? AppColors.primary
: value.isEmpty
? Colors.grey.shade400
: Colors.grey.shade800,
), ),
maxLines: isMultiLine ? 3 : 1, maxLines: isMultiLine ? 3 : 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@ -685,6 +798,12 @@ class WargaProfileView extends GetView<WargaDashboardController> {
], ],
), ),
), ),
if (isImportant)
Icon(
Icons.priority_high,
size: 16,
color: AppColors.primary.withOpacity(0.7),
),
], ],
), ),
); );
@ -821,7 +940,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar( Get.snackbar(
'Info', 'Info',
'Fitur Ubah Password akan segera tersedia', 'Fitur Ubah Password akan segera tersedia',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
); );
}, },
), ),
@ -933,4 +1052,54 @@ class WargaProfileView extends GetView<WargaDashboardController> {
), ),
); );
} }
// Fungsi untuk memformat tanggal lahir ke format Indonesia
String formatTanggalLahir(String tanggal) {
if (tanggal.isEmpty) return '';
try {
// Coba parse tanggal dari berbagai format yang mungkin
DateTime? dateTime;
// Coba format yyyy-MM-dd
if (tanggal.contains('-')) {
final parts = tanggal.split('-');
if (parts.length == 3) {
dateTime = DateTime.tryParse(tanggal);
}
}
// Coba format dd/MM/yyyy
else if (tanggal.contains('/')) {
final parts = tanggal.split('/');
if (parts.length == 3) {
dateTime = DateTime.tryParse('${parts[2]}-${parts[1]}-${parts[0]}');
}
}
// Jika tidak bisa parse, kembalikan nilai asli
if (dateTime == null) return tanggal;
// Daftar nama bulan dalam bahasa Indonesia
final List<String> namaBulan = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
];
// Format tanggal ke format Indonesia: dd Bulan yyyy
return '${dateTime.day} ${namaBulan[dateTime.month - 1]} ${dateTime.year}';
} catch (e) {
// Jika ada error, kembalikan nilai asli
return tanggal;
}
}
} }

View File

@ -570,8 +570,34 @@ class WargaSewaView extends GetView<WargaSewaController> {
if ((rental['status'] ?? '').toString().toUpperCase() == if ((rental['status'] ?? '').toString().toUpperCase() ==
'MENUNGGU PEMBAYARAN' && 'MENUNGGU PEMBAYARAN' &&
rental['updated_at'] != null) rental['updated_at'] != null)
CountdownTimerWidget( Builder(
updatedAt: DateTime.parse(rental['updated_at']), builder: (context) {
final updatedAtStr = rental['updated_at'];
debugPrint('DEBUG status: ${rental['status']}');
debugPrint('DEBUG updated_at: $updatedAtStr');
if (updatedAtStr != null) {
try {
final updatedAt = DateTime.parse(updatedAtStr);
debugPrint(
'DEBUG parsed updated_at: ${updatedAt.toIso8601String()}',
);
return CountdownTimerWidget(
updatedAt: updatedAt,
);
} catch (e) {
debugPrint('ERROR parsing updated_at: $e');
return Text(
'Format tanggal salah',
style: TextStyle(
color: Colors.red,
fontSize: 12,
),
);
}
}
return SizedBox.shrink();
},
), ),
], ],
), ),
@ -1856,32 +1882,46 @@ class CountdownTimerWidget extends StatefulWidget {
class _CountdownTimerWidgetState extends State<CountdownTimerWidget> { class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
late Duration remaining; late Duration remaining;
Timer? timer; Timer? timer;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
debugPrint(
'CountdownTimerWidget initialized with updatedAt: ${widget.updatedAt}',
);
updateRemaining(); updateRemaining();
timer = Timer.periodic( timer = Timer.periodic(
const Duration(seconds: 1), const Duration(seconds: 1),
(_) => updateRemaining(), (_) => updateRemaining(),
); );
print('DEBUG updated_at: ${widget.updatedAt}');
} }
void updateRemaining() { void updateRemaining() {
final now = DateTime.now(); final now = DateTime.now();
final deadline = widget.updatedAt.add(const Duration(hours: 1)); final deadline = widget.updatedAt.add(const Duration(hours: 1));
debugPrint('Current time: $now');
debugPrint('Deadline: $deadline');
setState(() { setState(() {
remaining = deadline.difference(now); remaining = deadline.difference(now);
debugPrint('Remaining time: ${remaining.inSeconds} seconds');
if (remaining.isNegative) { if (remaining.isNegative) {
debugPrint('Countdown expired, setting to zero');
remaining = Duration.zero; remaining = Duration.zero;
timer?.cancel(); timer?.cancel();
widget.onTimeout?.call(); if (widget.onTimeout != null) {
debugPrint('Calling onTimeout callback');
widget.onTimeout?.call();
}
} }
}); });
} }
@override @override
void dispose() { void dispose() {
debugPrint('CountdownTimerWidget disposed');
timer?.cancel(); timer?.cancel();
super.dispose(); super.dispose();
} }
@ -1891,9 +1931,15 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
if (remaining.inSeconds <= 0) { if (remaining.inSeconds <= 0) {
return Text('Waktu habis', style: TextStyle(color: Colors.red)); return Text('Waktu habis', style: TextStyle(color: Colors.red));
} }
final h = remaining.inHours; final h = remaining.inHours;
final m = remaining.inMinutes % 60; final m = remaining.inMinutes % 60;
final s = remaining.inSeconds % 60; final s = remaining.inSeconds % 60;
final timeString =
'${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
debugPrint('Rendering countdown: $timeString');
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration( decoration: BoxDecoration(
@ -1907,7 +1953,7 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
Icon(Icons.timer_outlined, size: 14, color: Colors.red), Icon(Icons.timer_outlined, size: 14, color: Colors.red),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'Bayar dalam ${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}', 'Bayar dalam $timeString',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@ -41,7 +41,7 @@ class NavigationService extends GetxService {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID aset tidak valid', 'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -63,7 +63,7 @@ class NavigationService extends GetxService {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID sewa tidak valid', 'ID sewa tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -87,7 +87,7 @@ class NavigationService extends GetxService {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'ID paket tidak valid', 'ID paket tidak valid',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -133,7 +133,7 @@ class NavigationService extends GetxService {
void toProfile() { void toProfile() {
debugPrint('🧭 Navigating to Profile'); debugPrint('🧭 Navigating to Profile');
setNavIndex(2); // Profile tab setNavIndex(2); // Profile tab
Get.offNamed(Routes.PROFILE); Get.offAllNamed(Routes.PROFILE);
} }
/// Navigasi ke dashboard sesuai role /// Navigasi ke dashboard sesuai role

View File

@ -48,10 +48,6 @@ void main() async {
debugPrint('❌ Error initializing auth provider: $e'); debugPrint('❌ Error initializing auth provider: $e');
} }
// Pre-register the dashboard controller to fix dependency issues
Get.put(PetugasBumdesDashboardController(), permanent: true);
debugPrint('✅ PetugasBumdesDashboardController initialized globally');
// Register services yang akan digunakan secara global // Register services yang akan digunakan secara global
ServiceManager.registerServices(); ServiceManager.registerServices();

View File

@ -1,68 +1,68 @@
{ {
"images" : [ "info": {
{ "version": 1,
"size" : "16x16", "author": "xcode"
"idiom" : "mac",
"filename" : "app_icon_16.png",
"scale" : "1x"
}, },
{ "images": [
"size" : "16x16", {
"idiom" : "mac", "size": "16x16",
"filename" : "app_icon_32.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_16.png",
}, "scale": "1x"
{ },
"size" : "32x32", {
"idiom" : "mac", "size": "16x16",
"filename" : "app_icon_32.png", "idiom": "mac",
"scale" : "1x" "filename": "app_icon_32.png",
}, "scale": "2x"
{ },
"size" : "32x32", {
"idiom" : "mac", "size": "32x32",
"filename" : "app_icon_64.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_32.png",
}, "scale": "1x"
{ },
"size" : "128x128", {
"idiom" : "mac", "size": "32x32",
"filename" : "app_icon_128.png", "idiom": "mac",
"scale" : "1x" "filename": "app_icon_64.png",
}, "scale": "2x"
{ },
"size" : "128x128", {
"idiom" : "mac", "size": "128x128",
"filename" : "app_icon_256.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_128.png",
}, "scale": "1x"
{ },
"size" : "256x256", {
"idiom" : "mac", "size": "128x128",
"filename" : "app_icon_256.png", "idiom": "mac",
"scale" : "1x" "filename": "app_icon_256.png",
}, "scale": "2x"
{ },
"size" : "256x256", {
"idiom" : "mac", "size": "256x256",
"filename" : "app_icon_512.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_256.png",
}, "scale": "1x"
{ },
"size" : "512x512", {
"idiom" : "mac", "size": "256x256",
"filename" : "app_icon_512.png", "idiom": "mac",
"scale" : "1x" "filename": "app_icon_512.png",
}, "scale": "2x"
{ },
"size" : "512x512", {
"idiom" : "mac", "size": "512x512",
"filename" : "app_icon_1024.png", "idiom": "mac",
"scale" : "2x" "filename": "app_icon_512.png",
} "scale": "1x"
], },
"info" : { {
"version" : 1, "size": "512x512",
"author" : "xcode" "idiom": "mac",
} "filename": "app_icon_1024.png",
} "scale": "2x"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -33,6 +33,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.7" version: "4.0.7"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -97,6 +105,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
url: "https://pub.dev"
source: hosted
version: "2.0.3"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
clock: clock:
dependency: transitive dependency: transitive
description: description:
@ -270,6 +294,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.1.5" version: "0.1.5"
flutter_launcher_icons:
dependency: "direct dev"
description:
name: flutter_launcher_icons
sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
url: "https://pub.dev"
source: hosted
version: "0.13.1"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -509,6 +541,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.6.7" version: "0.6.7"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
jwt_decode: jwt_decode:
dependency: transitive dependency: transitive
description: description:
@ -1146,6 +1186,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.5.0" version: "6.5.0"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
yet_another_json_isolate: yet_another_json_isolate:
dependency: transitive dependency: transitive
description: description:

View File

@ -72,6 +72,28 @@ dev_dependencies:
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
# App icon generation tool
flutter_launcher_icons: ^0.13.1
# App icon configuration
flutter_launcher_icons:
android: "launcher_icon"
ios: true
image_path: "assets/images/logo_app.png"
min_sdk_android: 21 # android min sdk min:16, default 21
web:
generate: true
image_path: "assets/images/logo_app.png"
background_color: "#ffffff"
theme_color: "#ffffff"
windows:
generate: true
image_path: "assets/images/logo_app.png"
icon_size: 48 # min:48, max:256, default: 48
macos:
generate: true
image_path: "assets/images/logo_app.png"
# For information on the generic Dart part of this file, see the # For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec # following page: https://dart.dev/tools/pub/pubspec

Binary file not shown.

Before

Width:  |  Height:  |  Size: 917 B

After

Width:  |  Height:  |  Size: 480 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -3,8 +3,8 @@
"short_name": "bumrent_app", "short_name": "bumrent_app",
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"background_color": "#0175C2", "background_color": "#ffffff",
"theme_color": "#0175C2", "theme_color": "#ffffff",
"description": "A new Flutter project.", "description": "A new Flutter project.",
"orientation": "portrait-primary", "orientation": "portrait-primary",
"prefer_related_applications": false, "prefer_related_applications": false,
@ -32,4 +32,4 @@
"purpose": "maskable" "purpose": "maskable"
} }
] ]
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB