fitur petugas

This commit is contained in:
Andreas Malvino
2025-06-22 09:25:58 +07:00
parent c4dd4fdfa2
commit 8284c93aa5
48 changed files with 8688 additions and 3436 deletions

View File

@ -8,6 +8,10 @@ class AuthController extends GetxController {
final emailController = TextEditingController();
final passwordController = TextEditingController();
final formKey = GlobalKey<FormState>();
final nameController = TextEditingController();
final confirmPasswordController = TextEditingController();
final RxBool isConfirmPasswordVisible = false.obs;
// Form fields for registration
final RxString email = ''.obs;
@ -15,6 +19,7 @@ class AuthController extends GetxController {
final RxString nik = ''.obs;
final RxString phoneNumber = ''.obs;
final RxString selectedRole = 'WARGA'.obs; // Default role
final RxString alamatLengkap = ''.obs;
// Form status
final RxBool isLoading = false.obs;
@ -28,6 +33,10 @@ class AuthController extends GetxController {
isPasswordVisible.value = !isPasswordVisible.value;
}
void toggleConfirmPasswordVisibility() {
isConfirmPasswordVisible.value = !isConfirmPasswordVisible.value;
}
// Change role selection
void setRole(String? role) {
if (role != null) {
@ -172,6 +181,8 @@ class AuthController extends GetxController {
void onClose() {
emailController.dispose();
passwordController.dispose();
nameController.dispose();
confirmPasswordController.dispose();
super.onClose();
}
@ -181,7 +192,8 @@ class AuthController extends GetxController {
if (email.value.isEmpty ||
password.value.isEmpty ||
nik.value.isEmpty ||
phoneNumber.value.isEmpty) {
phoneNumber.value.isEmpty ||
alamatLengkap.value.isEmpty) {
errorMessage.value = 'Semua field harus diisi';
return;
}
@ -222,6 +234,7 @@ class AuthController extends GetxController {
data: {
'nik': nik.value.trim(),
'phone_number': phoneNumber.value.trim(),
'alamat_lengkap': alamatLengkap.value.trim(),
'role': selectedRole.value,
},
);

View File

@ -26,12 +26,8 @@ class ForgotPasswordView extends GetView<AuthController> {
Opacity(
opacity: 0.03,
child: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/pattern.png'),
repeat: ImageRepeat.repeat,
scale: 4.0,
),
decoration: BoxDecoration(
color: Colors.blue[50], // Temporary solid color
),
),
),

View File

@ -30,12 +30,8 @@ class LoginView extends GetView<AuthController> {
Opacity(
opacity: 0.03,
child: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/pattern.png'),
repeat: ImageRepeat.repeat,
scale: 4.0,
),
decoration: BoxDecoration(
color: Colors.blue[50], // Temporary solid color
),
),
),
@ -89,7 +85,6 @@ class LoginView extends GetView<AuthController> {
_buildHeader(),
const SizedBox(height: 40),
_buildLoginCard(),
const SizedBox(height: 24),
_buildRegisterLink(),
const SizedBox(height: 30),
],
@ -161,7 +156,7 @@ class LoginView extends GetView<AuthController> {
prefixIcon: Icons.email_outlined,
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: 24),
const SizedBox(height: 12),
// Password field
_buildInputLabel('Password'),
@ -204,7 +199,6 @@ class LoginView extends GetView<AuthController> {
),
),
),
const SizedBox(height: 32),
// Login button
Obx(

View File

@ -187,7 +187,7 @@ class RegistrationView extends GetView<AuthController> {
),
const SizedBox(height: 4),
Text(
'Pendaftaran hanya dapat dilakukan oleh warga dan mitra yang sudah terverivikasi. Silahkan hubungi petugas atau kunjungi kantor untuk informasi lebih lanjut.',
'Setelah pendaftaran lengkapi data diri untuk dapat melakukan sewa',
style: TextStyle(
fontSize: 13,
color: AppColors.textPrimary,
@ -203,121 +203,32 @@ class RegistrationView extends GetView<AuthController> {
}
Widget _buildRegistrationForm() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Email Input
_buildInputLabel('Email'),
const SizedBox(height: 8),
_buildEmailField(),
const SizedBox(height: 20),
// Password Input
_buildInputLabel('Password'),
const SizedBox(height: 8),
_buildPasswordField(),
const SizedBox(height: 20),
// NIK Input
_buildInputLabel('NIK'),
const SizedBox(height: 8),
_buildNikField(),
const SizedBox(height: 20),
// Phone Number Input
_buildInputLabel('No. Hp'),
const SizedBox(height: 8),
_buildPhoneField(),
const SizedBox(height: 20),
// Role Selection Dropdown
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Daftar Sebagai',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.black87,
),
),
const SizedBox(height: 8),
Container(
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey[300]!, width: 1),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Obx(
() => DropdownButtonHideUnderline(
child: DropdownButton<String>(
isExpanded: true,
value: controller.selectedRole.value,
hint: const Text('Pilih Peran'),
items: [
DropdownMenuItem(
value: 'WARGA',
child: const Text('Warga'),
),
DropdownMenuItem(
value: 'PETUGAS_MITRA',
child: const Text('Mitra'),
),
],
onChanged: (value) {
controller.setRole(value);
},
icon: const Icon(Icons.arrow_drop_down),
style: const TextStyle(
color: Colors.black87,
fontSize: 14,
),
),
),
),
),
),
],
),
const SizedBox(height: 20),
// Error message
Obx(
() =>
controller.errorMessage.value.isNotEmpty
? Container(
margin: const EdgeInsets.only(top: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.errorLight,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
Icons.error_outline,
color: AppColors.error,
size: 20,
),
const SizedBox(width: 8),
Expanded(
child: Text(
controller.errorMessage.value,
style: TextStyle(
color: AppColors.error,
fontSize: 13,
),
),
),
],
),
)
: const SizedBox.shrink(),
),
],
return Form(
key: controller.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildInputLabel('Email'),
_buildEmailField(),
const SizedBox(height: 16),
_buildInputLabel('Password'),
_buildPasswordField(),
const SizedBox(height: 16),
_buildInputLabel('Konfirmasi Password'),
_buildConfirmPasswordField(),
const SizedBox(height: 16),
_buildInputLabel('Nama Lengkap'),
_buildNameField(),
const SizedBox(height: 16),
_buildInputLabel('No HP'),
_buildPhoneField(),
const SizedBox(height: 16),
_buildInputLabel('Alamat Lengkap'),
_buildAlamatField(),
const SizedBox(height: 16),
// Removed: NIK, No HP, and Dropdown Daftar Sebagai
],
),
);
}
@ -415,78 +326,101 @@ class RegistrationView extends GetView<AuthController> {
);
}
Widget _buildNikField() {
return Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.shadow,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: TextField(
onChanged: (value) => controller.nik.value = value,
keyboardType: TextInputType.number,
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
Widget _buildConfirmPasswordField() {
return Obx(
() => TextFormField(
controller: controller.confirmPasswordController,
obscureText: !controller.isConfirmPasswordVisible.value,
decoration: InputDecoration(
hintText: 'Masukkan NIK anda',
hintStyle: TextStyle(color: AppColors.textLight),
prefixIcon: Icon(
Icons.credit_card_outlined,
color: AppColors.primary,
hintText: 'Masukkan ulang password anda',
suffixIcon: IconButton(
icon: Icon(
controller.isConfirmPasswordVisible.value
? Icons.visibility
: Icons.visibility_off,
),
onPressed: controller.toggleConfirmPasswordVisibility,
),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 16),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Konfirmasi password tidak boleh kosong';
}
if (value != controller.passwordController.text) {
return 'Password tidak cocok';
}
return null;
},
),
);
}
Widget _buildPhoneField() {
return Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppColors.shadow,
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: TextField(
onChanged: (value) => controller.phoneNumber.value = value,
keyboardType: TextInputType.phone,
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
decoration: InputDecoration(
hintText: 'Masukkan nomor HP anda',
hintStyle: TextStyle(color: AppColors.textLight),
prefixIcon: Icon(Icons.phone_outlined, color: AppColors.primary),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 16),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide(color: AppColors.primary, width: 1.5),
),
Widget _buildNameField() {
return TextFormField(
controller: controller.nameController,
decoration: InputDecoration(
hintText: 'Masukkan nama lengkap anda',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama lengkap tidak boleh kosong';
}
return null;
},
);
}
Widget _buildPhoneField() {
return TextFormField(
keyboardType: TextInputType.phone,
decoration: InputDecoration(
hintText: 'Masukkan nomor HP anda',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
onChanged: (value) => controller.phoneNumber.value = value,
validator: (value) {
if (value == null || value.isEmpty) {
return 'No HP tidak boleh kosong';
}
if (!value.startsWith('08') || value.length < 10) {
return 'Nomor HP tidak valid (harus diawali 08 dan minimal 10 digit)';
}
return null;
},
);
}
Widget _buildAlamatField() {
return TextFormField(
decoration: InputDecoration(
hintText: 'Masukkan alamat lengkap anda',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
onChanged: (value) => controller.alamatLengkap.value = value,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Alamat lengkap tidak boleh kosong';
}
return null;
},
);
}

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart';
import '../controllers/petugas_aset_controller.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart';
import '../../../data/providers/aset_provider.dart';
class PetugasAsetBinding extends Bindings {
@override
@ -10,6 +11,7 @@ class PetugasAsetBinding extends Bindings {
Get.put(PetugasBumdesDashboardController(), permanent: true);
}
Get.lazyPut<AsetProvider>(() => AsetProvider());
Get.lazyPut<PetugasAsetController>(() => PetugasAsetController());
}
}

View File

@ -1,9 +1,14 @@
import 'package:get/get.dart';
import '../controllers/petugas_sewa_controller.dart';
import '../../../data/providers/aset_provider.dart';
class PetugasDetailSewaBinding extends Bindings {
@override
void dependencies() {
// Ensure AsetProvider is registered
if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true);
}
// Memastikan controller sudah tersedia
Get.lazyPut<PetugasSewaController>(
() => PetugasSewaController(),

View File

@ -1,15 +1,25 @@
import 'package:get/get.dart';
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
import '../controllers/petugas_paket_controller.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart';
class PetugasPaketBinding extends Bindings {
@override
void dependencies() {
// Register AsetProvider first
if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true);
}
// Ensure dashboard controller is registered
if (!Get.isRegistered<PetugasBumdesDashboardController>()) {
Get.put(PetugasBumdesDashboardController(), permanent: true);
}
Get.lazyPut<PetugasPaketController>(() => PetugasPaketController());
// Register the controller
Get.lazyPut<PetugasPaketController>(
() => PetugasPaketController(),
fenix: true,
);
}
}

View File

@ -1,9 +1,14 @@
import 'package:get/get.dart';
import '../controllers/petugas_sewa_controller.dart';
import '../../../data/providers/aset_provider.dart';
class PetugasSewaBinding extends Bindings {
@override
void dependencies() {
// Ensure AsetProvider is registered
if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true);
}
Get.lazyPut<PetugasSewaController>(() => PetugasSewaController());
}
}

View File

@ -1,6 +1,11 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../data/providers/aset_provider.dart';
import '../../../data/models/aset_model.dart';
class PetugasAsetController extends GetxController {
final AsetProvider _asetProvider = Get.find<AsetProvider>();
// Observable lists for asset data
final asetList = <Map<String, dynamic>>[].obs;
final filteredAsetList = <Map<String, dynamic>>[].obs;
@ -27,95 +32,100 @@ class PetugasAsetController extends GetxController {
loadAsetData();
}
// Load sample asset data (would be replaced with API call in production)
// Load asset data from AsetProvider
Future<void> loadAsetData() async {
isLoading.value = true;
try {
// Simulate API call with a delay
await Future.delayed(const Duration(seconds: 1));
isLoading.value = true;
debugPrint('PetugasAsetController: Starting to load asset data...');
// Sample assets data
final sampleData = [
{
'id': '1',
'nama': 'Meja Rapat',
'kategori': 'Furniture',
'jenis': 'Sewa', // Added jenis field
'harga': 50000,
'satuan': 'per hari',
'stok': 10,
'deskripsi':
'Meja rapat kayu jati ukuran besar untuk acara pertemuan',
'gambar': 'https://example.com/meja.jpg',
'tersedia': true,
},
{
'id': '2',
'nama': 'Kursi Taman',
'kategori': 'Furniture',
'jenis': 'Sewa', // Added jenis field
'harga': 10000,
'satuan': 'per hari',
'stok': 50,
'deskripsi': 'Kursi taman plastik yang nyaman untuk acara outdoor',
'gambar': 'https://example.com/kursi.jpg',
'tersedia': true,
},
{
'id': '3',
'nama': 'Proyektor',
'kategori': 'Elektronik',
'jenis': 'Sewa', // Added jenis field
'harga': 100000,
'satuan': 'per hari',
'stok': 5,
'deskripsi': 'Proyektor HD dengan brightness tinggi',
'gambar': 'https://example.com/proyektor.jpg',
'tersedia': true,
},
{
'id': '4',
'nama': 'Sound System',
'kategori': 'Elektronik',
'jenis': 'Langganan', // Added jenis field
'harga': 200000,
'satuan': 'per bulan',
'stok': 3,
'deskripsi': 'Sound system lengkap dengan speaker dan mixer',
'gambar': 'https://example.com/sound.jpg',
'tersedia': false,
},
{
'id': '5',
'nama': 'Mobil Pick Up',
'kategori': 'Kendaraan',
'jenis': 'Langganan', // Added jenis field
'harga': 250000,
'satuan': 'per bulan',
'stok': 2,
'deskripsi': 'Mobil pick up untuk mengangkut barang',
'gambar': 'https://example.com/pickup.jpg',
'tersedia': true,
},
{
'id': '6',
'nama': 'Internet Fiber',
'kategori': 'Elektronik',
'jenis': 'Langganan', // Added jenis field
'harga': 350000,
'satuan': 'per bulan',
'stok': 15,
'deskripsi': 'Paket internet fiber 100Mbps untuk kantor',
'gambar': 'https://example.com/internet.jpg',
'tersedia': true,
},
];
// Fetch data using AsetProvider
final asetData = await _asetProvider.getSewaAsets();
debugPrint(
'PetugasAsetController: Fetched ${asetData.length} assets from Supabase',
);
asetList.assignAll(sampleData);
applyFilters(); // Apply default filters
} catch (e) {
print('Error loading asset data: $e');
if (asetData.isEmpty) {
debugPrint('PetugasAsetController: No assets found in Supabase');
}
final List<Map<String, dynamic>> mappedAsets = [];
int index = 0; // Initialize index counter
for (var aset in asetData) {
String displayKategori = 'Umum'; // Placeholder for descriptive category
// Attempt to derive a more specific category from description if needed, or add to AsetModel
if (aset.deskripsi.toLowerCase().contains('meja') ||
aset.deskripsi.toLowerCase().contains('kursi')) {
displayKategori = 'Furniture';
} else if (aset.deskripsi.toLowerCase().contains('proyektor') ||
aset.deskripsi.toLowerCase().contains('sound') ||
aset.deskripsi.toLowerCase().contains('internet')) {
displayKategori = 'Elektronik';
} else if (aset.deskripsi.toLowerCase().contains('mobil') ||
aset.deskripsi.toLowerCase().contains('kendaraan')) {
displayKategori = 'Kendaraan';
}
final map = {
'id': aset.id,
'nama': aset.nama,
'deskripsi': aset.deskripsi,
'harga':
aset.satuanWaktuSewa.isNotEmpty
? aset.satuanWaktuSewa.first['harga']
: 0,
'status': aset.status,
'kategori': displayKategori,
'jenis': aset.jenis ?? 'Sewa', // Add this line with default value
'imageUrl': aset.imageUrl ?? 'https://via.placeholder.com/150',
'satuan_waktu':
aset.satuanWaktuSewa.isNotEmpty
? aset.satuanWaktuSewa.first['nama_satuan_waktu'] ?? 'Hari'
: 'Hari',
'satuanWaktuSewa': aset.satuanWaktuSewa.toList(),
};
debugPrint('Mapped asset #$index: $map');
mappedAsets.add(map);
index++;
debugPrint('Deskripsi: ${aset.deskripsi}');
debugPrint('Kategori (from AsetModel): ${aset.kategori}');
debugPrint('Status: ${aset.status}');
debugPrint('Mapped Kategori for Petugas View: ${map['kategori']}');
debugPrint('Mapped Jenis for Petugas View: ${map['jenis']}');
debugPrint('--------------------------------');
}
// Populate asetList with fetched data and apply filters
debugPrint(
'PetugasAsetController: Mapped ${mappedAsets.length} assets for display',
);
asetList.assignAll(mappedAsets); // Make data available to UI
debugPrint(
'PetugasAsetController: asetList now has ${asetList.length} items',
);
applyFilters(); // Apply initial filters
debugPrint(
'PetugasAsetController: Applied filters. filteredAsetList has ${filteredAsetList.length} items',
);
debugPrint(
'PetugasAsetController: Data loading complete. Asset list populated and filters applied.',
);
debugPrint(
'PetugasAsetController: First asset name: ${mappedAsets.isNotEmpty ? mappedAsets[0]['nama'] : 'No assets'}',
);
} catch (e, stackTrace) {
debugPrint('PetugasAsetController: Error loading asset data: $e');
debugPrint('PetugasAsetController: StackTrace: $stackTrace');
// Optionally, show a snackbar or error message to the user
Get.snackbar(
'Error Memuat Data',
'Gagal mengambil data aset dari server. Silakan coba lagi nanti.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
@ -170,8 +180,10 @@ class PetugasAsetController extends GetxController {
}
// Change tab (Sewa or Langganan)
void changeTab(int index) {
Future<void> changeTab(int index) async {
selectedTabIndex.value = index;
// Reload data when changing tabs to ensure we have the correct data for the selected tab
await loadAsetData();
applyFilters();
}

View File

@ -1,6 +1,10 @@
import 'package:get/get.dart';
import '../../../data/providers/auth_provider.dart';
import '../../../routes/app_routes.dart';
import '../../../services/sewa_service.dart';
import '../../../services/service_manager.dart';
import '../../../data/models/pembayaran_model.dart';
import '../../../services/pembayaran_service.dart';
class PetugasBumdesDashboardController extends GetxController {
AuthProvider? _authProvider;
@ -8,6 +12,8 @@ class PetugasBumdesDashboardController extends GetxController {
// Reactive variables
final userEmail = ''.obs;
final currentTabIndex = 0.obs;
final avatarUrl = ''.obs;
final userName = ''.obs;
// Revenue Statistics
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
@ -20,7 +26,7 @@ class PetugasBumdesDashboardController extends GetxController {
final persentaseSewa = 100.obs;
// Revenue Trends (last 6 months)
final trendPendapatan = [4.2, 5.1, 4.8, 6.2, 7.2, 8.5].obs; // in millions
final trendPendapatan = <double>[].obs; // 6 bulan terakhir
// Status Counters for Sewa Aset
final terlaksanaCount = 5.obs;
@ -43,42 +49,128 @@ class PetugasBumdesDashboardController extends GetxController {
final tagihanAktifCountSewa = 7.obs;
final periksaPembayaranCountSewa = 2.obs;
// Statistik pendapatan
final totalPendapatan = 0.obs;
final pendapatanBulanIni = 0.obs;
final pendapatanBulanLalu = 0.obs;
final pendapatanTunai = 0.obs;
final pendapatanTransfer = 0.obs;
final trenPendapatan = <int>[].obs; // 6 bulan terakhir
// Dashboard statistics
final pembayaranStats = <String, dynamic>{}.obs;
final isStatsLoading = true.obs;
@override
void onInit() {
super.onInit();
try {
_authProvider = Get.find<AuthProvider>();
userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email';
fetchPetugasAvatar();
fetchPetugasName();
} catch (e) {
print('Error finding AuthProvider: $e');
userEmail.value = 'Tidak ada email';
}
// In a real app, these counts would be fetched from backend
// loadStatusCounts();
print('✅ PetugasBumdesDashboardController initialized successfully');
print('\u2705 PetugasBumdesDashboardController initialized successfully');
countSewaByStatus();
fetchPembayaranStats();
}
// Method to load status counts from backend
// Future<void> loadStatusCounts() async {
// try {
// final response = await _asetProvider.getSewaStatusCounts();
// if (response != null) {
// terlaksanaCount.value = response['terlaksana'] ?? 0;
// dijadwalkanCount.value = response['dijadwalkan'] ?? 0;
// aktifCount.value = response['aktif'] ?? 0;
// dibatalkanCount.value = response['dibatalkan'] ?? 0;
// menungguPembayaranCount.value = response['menunggu_pembayaran'] ?? 0;
// periksaPembayaranCount.value = response['periksa_pembayaran'] ?? 0;
// diterimaCount.value = response['diterima'] ?? 0;
// pembayaranDendaCount.value = response['pembayaran_denda'] ?? 0;
// periksaPembayaranDendaCount.value = response['periksa_pembayaran_denda'] ?? 0;
// selesaiCount.value = response['selesai'] ?? 0;
// }
// } catch (e) {
// print('Error loading status counts: $e');
// }
// }
Future<void> countSewaByStatus() async {
try {
final data = await SewaService().fetchAllSewa();
menungguPembayaranCount.value =
data.where((s) => s.status == 'MENUNGGU PEMBAYARAN').length;
periksaPembayaranCount.value =
data.where((s) => s.status == 'PERIKSA PEMBAYARAN').length;
diterimaCount.value = data.where((s) => s.status == 'DITERIMA').length;
pembayaranDendaCount.value =
data.where((s) => s.status == 'PEMBAYARAN DENDA').length;
periksaPembayaranDendaCount.value =
data.where((s) => s.status == 'PERIKSA PEMBAYARAN DENDA').length;
selesaiCount.value = data.where((s) => s.status == 'SELESAI').length;
print(
'Count for MENUNGGU PEMBAYARAN: \\${menungguPembayaranCount.value}',
);
print('Count for PERIKSA PEMBAYARAN: \\${periksaPembayaranCount.value}');
print('Count for DITERIMA: \\${diterimaCount.value}');
print('Count for PEMBAYARAN DENDA: \\${pembayaranDendaCount.value}');
print(
'Count for PERIKSA PEMBAYARAN DENDA: \\${periksaPembayaranDendaCount.value}',
);
print('Count for SELESAI: \\${selesaiCount.value}');
} catch (e) {
print('Error counting sewa by status: $e');
}
}
Future<void> fetchPembayaranStats() async {
isStatsLoading.value = true;
try {
final stats = await PembayaranService().fetchStats();
pembayaranStats.value = stats;
// Set trendPendapatan from stats['trendPerMonth'] if available
if (stats['trendPerMonth'] != null) {
trendPendapatan.value = List<double>.from(stats['trendPerMonth']);
}
print('Pembayaran stats: $stats');
} catch (e, st) {
print('Error fetching pembayaran stats: $e\n$st');
pembayaranStats.value = {};
trendPendapatan.value = [];
}
isStatsLoading.value = false;
}
Future<void> fetchPetugasAvatar() async {
try {
final userId = _authProvider?.getCurrentUserId();
if (userId == null) return;
final client = _authProvider!.client;
final data =
await client
.from('petugas_bumdes')
.select('avatar')
.eq('id', userId)
.maybeSingle();
if (data != null &&
data['avatar'] != null &&
data['avatar'].toString().isNotEmpty) {
avatarUrl.value = data['avatar'].toString();
} else {
avatarUrl.value = '';
}
} catch (e) {
print('Error fetching petugas avatar: $e');
avatarUrl.value = '';
}
}
Future<void> fetchPetugasName() async {
try {
final userId = _authProvider?.getCurrentUserId();
if (userId == null) return;
final client = _authProvider!.client;
final data =
await client
.from('petugas_bumdes')
.select('nama')
.eq('id', userId)
.maybeSingle();
if (data != null &&
data['nama'] != null &&
data['nama'].toString().isNotEmpty) {
userName.value = data['nama'].toString();
} else {
userName.value = '';
}
} catch (e) {
print('Error fetching petugas name: $e');
userName.value = '';
}
}
void changeTab(int index) {
try {

View File

@ -1,24 +1,24 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:intl/intl.dart' show NumberFormat;
import 'package:logger/logger.dart';
import 'package:bumrent_app/app/data/models/paket_model.dart';
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
class PetugasPaketController extends GetxController {
final isLoading = false.obs;
final searchQuery = ''.obs;
final selectedCategory = 'Semua'.obs;
final sortBy = 'Terbaru'.obs;
// Kategori untuk filter
final categories = <String>[
'Semua',
'Pesta',
'Rapat',
'Olahraga',
'Pernikahan',
'Lainnya',
];
// Opsi pengurutan
final sortOptions = <String>[
// Dependencies
final AsetProvider _asetProvider = Get.find<AsetProvider>();
// State
final RxBool isLoading = false.obs;
final RxString searchQuery = ''.obs;
final RxString selectedCategory = 'Semua'.obs;
final RxString sortBy = 'Terbaru'.obs;
final RxList<PaketModel> packages = <PaketModel>[].obs;
final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
// Sort options for the dropdown
final List<String> sortOptions = [
'Terbaru',
'Terlama',
'Harga Tertinggi',
@ -26,175 +26,221 @@ class PetugasPaketController extends GetxController {
'Nama A-Z',
'Nama Z-A',
];
// Data dummy paket
final paketList = <Map<String, dynamic>>[].obs;
final filteredPaketList = <Map<String, dynamic>>[].obs;
// For backward compatibility
final RxList<Map<String, dynamic>> paketList = <Map<String, dynamic>>[].obs;
final RxList<Map<String, dynamic>> filteredPaketList = <Map<String, dynamic>>[].obs;
// Logger
late final Logger _logger;
@override
void onInit() {
super.onInit();
loadPaketData();
}
// Format harga ke Rupiah
String formatPrice(int price) {
final formatter = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
// Initialize logger
_logger = Logger(
printer: PrettyPrinter(
methodCount: 0,
errorMethodCount: 5,
colors: true,
printEmojis: true,
),
);
return formatter.format(price);
// Load initial data
fetchPackages();
}
// Load data paket dummy
Future<void> loadPaketData() async {
isLoading.value = true;
await Future.delayed(const Duration(milliseconds: 800)); // Simulasi loading
paketList.value = [
{
'id': '1',
'nama': 'Paket Pesta Ulang Tahun',
'kategori': 'Pesta',
'harga': 500000,
'deskripsi':
'Paket lengkap untuk acara ulang tahun. Termasuk 5 meja, 20 kursi, backdrop, dan sound system.',
'tersedia': true,
'created_at': '2023-08-10',
'items': [
{'nama': 'Meja Panjang', 'jumlah': 5},
{'nama': 'Kursi Plastik', 'jumlah': 20},
{'nama': 'Sound System', 'jumlah': 1},
{'nama': 'Backdrop', 'jumlah': 1},
],
'gambar': 'https://example.com/images/paket_ultah.jpg',
},
{
'id': '2',
'nama': 'Paket Rapat Sedang',
'kategori': 'Rapat',
'harga': 300000,
'deskripsi':
'Paket untuk rapat sedang. Termasuk 1 meja rapat besar, 10 kursi, proyektor, dan screen.',
'tersedia': true,
'created_at': '2023-09-05',
'items': [
{'nama': 'Meja Rapat', 'jumlah': 1},
{'nama': 'Kursi Kantor', 'jumlah': 10},
{'nama': 'Proyektor', 'jumlah': 1},
{'nama': 'Screen', 'jumlah': 1},
],
'gambar': 'https://example.com/images/paket_rapat.jpg',
},
{
'id': '3',
'nama': 'Paket Pesta Pernikahan',
'kategori': 'Pernikahan',
'harga': 1500000,
'deskripsi':
'Paket lengkap untuk acara pernikahan. Termasuk 20 meja, 100 kursi, sound system, dekorasi, dan tenda.',
'tersedia': true,
'created_at': '2023-10-12',
'items': [
{'nama': 'Meja Bundar', 'jumlah': 20},
{'nama': 'Kursi Tamu', 'jumlah': 100},
{'nama': 'Sound System Besar', 'jumlah': 1},
{'nama': 'Tenda 10x10', 'jumlah': 2},
{'nama': 'Set Dekorasi Pengantin', 'jumlah': 1},
],
'gambar': 'https://example.com/images/paket_nikah.jpg',
},
{
'id': '4',
'nama': 'Paket Olahraga Voli',
'kategori': 'Olahraga',
'harga': 200000,
'deskripsi':
'Paket perlengkapan untuk turnamen voli. Termasuk net, bola, dan tiang voli.',
'tersedia': false,
'created_at': '2023-07-22',
'items': [
{'nama': 'Net Voli', 'jumlah': 1},
{'nama': 'Bola Voli', 'jumlah': 3},
{'nama': 'Tiang Voli', 'jumlah': 2},
],
'gambar': 'https://example.com/images/paket_voli.jpg',
},
{
'id': '5',
'nama': 'Paket Pesta Anak',
'kategori': 'Pesta',
'harga': 350000,
'deskripsi':
'Paket untuk pesta ulang tahun anak-anak. Termasuk 3 meja, 15 kursi, dekorasi tema, dan sound system kecil.',
'tersedia': true,
'created_at': '2023-11-01',
'items': [
{'nama': 'Meja Anak', 'jumlah': 3},
{'nama': 'Kursi Anak', 'jumlah': 15},
{'nama': 'Set Dekorasi Tema', 'jumlah': 1},
{'nama': 'Sound System Kecil', 'jumlah': 1},
],
'gambar': 'https://example.com/images/paket_anak.jpg',
},
];
filterPaket();
isLoading.value = false;
}
// Filter paket berdasarkan search query dan kategori
void filterPaket() {
filteredPaketList.value =
paketList.where((paket) {
final matchesQuery =
paket['nama'].toString().toLowerCase().contains(
searchQuery.value.toLowerCase(),
) ||
paket['deskripsi'].toString().toLowerCase().contains(
searchQuery.value.toLowerCase(),
);
final matchesCategory =
selectedCategory.value == 'Semua' ||
paket['kategori'] == selectedCategory.value;
return matchesQuery && matchesCategory;
}).toList();
// Sort the filtered list
sortFilteredList();
}
// Sort the filtered list
void sortFilteredList() {
switch (sortBy.value) {
case 'Terbaru':
filteredPaketList.sort(
(a, b) => b['created_at'].compareTo(a['created_at']),
);
break;
case 'Terlama':
filteredPaketList.sort(
(a, b) => a['created_at'].compareTo(b['created_at']),
);
break;
case 'Harga Tertinggi':
filteredPaketList.sort((a, b) => b['harga'].compareTo(a['harga']));
break;
case 'Harga Terendah':
filteredPaketList.sort((a, b) => a['harga'].compareTo(b['harga']));
break;
case 'Nama A-Z':
filteredPaketList.sort((a, b) => a['nama'].compareTo(b['nama']));
break;
case 'Nama Z-A':
filteredPaketList.sort((a, b) => b['nama'].compareTo(a['nama']));
break;
/// Fetch packages from the API
Future<void> fetchPackages() async {
try {
isLoading.value = true;
_logger.i('🔄 [fetchPackages] Fetching packages...');
final result = await _asetProvider.getAllPaket();
if (result.isEmpty) {
_logger.w(' [fetchPackages] No packages found');
packages.clear();
filteredPackages.clear();
return;
}
packages.assignAll(result);
filteredPackages.assignAll(result);
// Update legacy list for backward compatibility
_updateLegacyPaketList();
_logger.i('✅ [fetchPackages] Successfully loaded ${result.length} packages');
} catch (e, stackTrace) {
_logger.e('❌ [fetchPackages] Error fetching packages',
error: e,
stackTrace: stackTrace);
Get.snackbar(
'Error',
'Gagal memuat data paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
/// Update legacy paketList for backward compatibility
void _updateLegacyPaketList() {
try {
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
final List<Map<String, dynamic>> legacyList = packages.map((pkg) {
return {
'id': pkg.id,
'nama': pkg.nama,
'deskripsi': pkg.deskripsi,
'harga': pkg.harga,
'kuantitas': pkg.kuantitas,
'status': pkg.status, // Add status to legacy mapping
'foto': pkg.foto,
'foto_paket': pkg.foto_paket,
'images': pkg.images,
'satuanWaktuSewa': pkg.satuanWaktuSewa,
'created_at': pkg.createdAt,
'updated_at': pkg.updatedAt,
};
}).toList();
paketList.assignAll(legacyList);
filteredPaketList.assignAll(legacyList);
_logger.d('✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages');
} catch (e, stackTrace) {
_logger.e('❌ [_updateLegacyPaketList] Error updating legacy list',
error: e,
stackTrace: stackTrace);
}
}
/// For backward compatibility
Future<void> loadPaketData() async {
_logger.d(' [loadPaketData] Using fetchPackages() instead');
await fetchPackages();
}
/// Filter packages based on search query and category
void filterPaket() {
try {
_logger.d('🔄 [filterPaket] Filtering packages...');
if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
filteredPackages.value = List.from(packages);
filteredPaketList.value = List.from(paketList);
} else {
// Filter new packages
filteredPackages.value = packages.where((paket) {
final matchesSearch = searchQuery.value.isEmpty ||
paket.nama.toLowerCase().contains(searchQuery.value.toLowerCase());
// For now, we're not using categories in the new model
// You can add category filtering if needed
final matchesCategory = selectedCategory.value == 'Semua';
return matchesSearch && matchesCategory;
}).toList();
// Also update legacy list for backward compatibility
filteredPaketList.value = paketList.where((paket) {
final matchesSearch = searchQuery.value.isEmpty ||
(paket['nama']?.toString() ?? '').toLowerCase()
.contains(searchQuery.value.toLowerCase());
// For legacy support, check if category exists
final matchesCategory = selectedCategory.value == 'Semua' ||
(paket['kategori']?.toString() ?? '') == selectedCategory.value;
return matchesSearch && matchesCategory;
}).toList();
}
sortFilteredList();
_logger.d('✅ [filterPaket] Filtered to ${filteredPackages.length} packages');
} catch (e, stackTrace) {
_logger.e('❌ [filterPaket] Error filtering packages',
error: e,
stackTrace: stackTrace);
}
}
/// Sort the filtered list based on the selected sort option
void sortFilteredList() {
try {
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
// Sort new packages
switch (sortBy.value) {
case 'Terbaru':
filteredPackages.sort((a, b) => b.createdAt.compareTo(a.createdAt));
break;
case 'Terlama':
filteredPackages.sort((a, b) => a.createdAt.compareTo(b.createdAt));
break;
case 'Harga Tertinggi':
filteredPackages.sort((a, b) => b.harga.compareTo(a.harga));
break;
case 'Harga Terendah':
filteredPackages.sort((a, b) => a.harga.compareTo(b.harga));
break;
case 'Nama A-Z':
filteredPackages.sort((a, b) => a.nama.compareTo(b.nama));
break;
case 'Nama Z-A':
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
break;
}
// Also sort legacy list for backward compatibility
switch (sortBy.value) {
case 'Terbaru':
filteredPaketList.sort((a, b) =>
((b['created_at'] ?? '') as String).compareTo((a['created_at'] ?? '') as String));
break;
case 'Terlama':
filteredPaketList.sort((a, b) =>
((a['created_at'] ?? '') as String).compareTo((b['created_at'] ?? '') as String));
break;
case 'Harga Tertinggi':
filteredPaketList.sort((a, b) =>
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int));
break;
case 'Harga Terendah':
filteredPaketList.sort((a, b) =>
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int));
break;
case 'Nama A-Z':
filteredPaketList.sort((a, b) =>
((a['nama'] ?? '') as String).compareTo((b['nama'] ?? '') as String));
break;
case 'Nama Z-A':
filteredPaketList.sort((a, b) =>
((b['nama'] ?? '') as String).compareTo((a['nama'] ?? '') as String));
break;
}
_logger.d('✅ [sortFilteredList] Sorted ${filteredPackages.length} packages');
} catch (e, stackTrace) {
_logger.e('❌ [sortFilteredList] Error sorting packages',
error: e,
stackTrace: stackTrace);
}
}
// Set search query dan filter paket
void setSearchQuery(String query) {
searchQuery.value = query;
@ -214,40 +260,134 @@ class PetugasPaketController extends GetxController {
}
// Tambah paket baru
void addPaket(Map<String, dynamic> paket) {
paketList.add(paket);
filterPaket();
Get.back();
Get.snackbar(
'Sukses',
'Paket baru berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
);
}
// Edit paket
void editPaket(String id, Map<String, dynamic> updatedPaket) {
final index = paketList.indexWhere((element) => element['id'] == id);
if (index >= 0) {
paketList[index] = updatedPaket;
Future<void> addPaket(Map<String, dynamic> paketData) async {
try {
isLoading.value = true;
// Convert to PaketModel
final newPaket = PaketModel.fromJson({
...paketData,
'id': DateTime.now().millisecondsSinceEpoch.toString(),
'created_at': DateTime.now().toIso8601String(),
'updated_at': DateTime.now().toIso8601String(),
});
// Add to the list
packages.add(newPaket);
_updateLegacyPaketList();
filterPaket();
Get.back();
Get.snackbar(
'Sukses',
'Paket berhasil diperbarui',
'Paket baru berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e, stackTrace) {
_logger.e('❌ [addPaket] Error adding package',
error: e,
stackTrace: stackTrace);
Get.snackbar(
'Error',
'Gagal menambahkan paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
// Edit paket
Future<void> editPaket(String id, Map<String, dynamic> updatedData) async {
try {
isLoading.value = true;
final index = packages.indexWhere((pkg) => pkg.id == id);
if (index >= 0) {
// Update the package
final updatedPaket = packages[index].copyWith(
nama: updatedData['nama']?.toString() ?? packages[index].nama,
deskripsi: updatedData['deskripsi']?.toString() ?? packages[index].deskripsi,
kuantitas: (updatedData['kuantitas'] is int)
? updatedData['kuantitas']
: (int.tryParse(updatedData['kuantitas']?.toString() ?? '0') ?? packages[index].kuantitas),
updatedAt: DateTime.now(),
);
packages[index] = updatedPaket;
_updateLegacyPaketList();
filterPaket();
Get.back();
Get.snackbar(
'Sukses',
'Paket berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
} catch (e, stackTrace) {
_logger.e('❌ [editPaket] Error updating package',
error: e,
stackTrace: stackTrace);
Get.snackbar(
'Error',
'Gagal memperbarui paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
// Hapus paket
void deletePaket(String id) {
paketList.removeWhere((element) => element['id'] == id);
filterPaket();
Get.snackbar(
'Sukses',
'Paket berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
);
Future<void> deletePaket(String id) async {
try {
isLoading.value = true;
// Remove from the main list
packages.removeWhere((pkg) => pkg.id == id);
_updateLegacyPaketList();
filterPaket();
Get.back();
Get.snackbar(
'Sukses',
'Paket berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} catch (e, stackTrace) {
_logger.e('❌ [deletePaket] Error deleting package',
error: e,
stackTrace: stackTrace);
Get.snackbar(
'Error',
'Gagal menghapus paket. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
} finally {
isLoading.value = false;
}
}
/// Format price to Rupiah currency
String formatPrice(num price) {
return 'Rp ${NumberFormat('#,##0', 'id_ID').format(price)}';
}
}

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../services/sewa_service.dart';
import '../../../data/models/rental_booking_model.dart';
import '../../../data/providers/aset_provider.dart';
class PetugasSewaController extends GetxController {
// Reactive variables
@ -7,7 +10,7 @@ class PetugasSewaController extends GetxController {
final searchQuery = ''.obs;
final orderIdQuery = ''.obs;
final selectedStatusFilter = 'Semua'.obs;
final filteredSewaList = <Map<String, dynamic>>[].obs;
final filteredSewaList = <SewaModel>[].obs;
// Filter options
final List<String> statusFilters = [
@ -15,13 +18,19 @@ class PetugasSewaController extends GetxController {
'Menunggu Pembayaran',
'Periksa Pembayaran',
'Diterima',
'Aktif',
'Dikembalikan',
'Selesai',
'Dibatalkan',
];
// Mock data for sewa list
final RxList<Map<String, dynamic>> sewaList = <Map<String, dynamic>>[].obs;
final RxList<SewaModel> sewaList = <SewaModel>[].obs;
// Payment option state (per sewa)
final Map<String, RxBool> isFullPaymentMap = {};
final Map<String, TextEditingController> nominalControllerMap = {};
final Map<String, RxString> paymentMethodMap = {};
@override
void onInit() {
@ -41,25 +50,21 @@ class PetugasSewaController extends GetxController {
void _updateFilteredList() {
filteredSewaList.value =
sewaList.where((sewa) {
// Apply search filter
final matchesSearch = sewa['nama_warga']
.toString()
.toLowerCase()
.contains(searchQuery.value.toLowerCase());
// Apply order ID filter if provided
final matchesOrderId =
orderIdQuery.value.isEmpty ||
sewa['order_id'].toString().toLowerCase().contains(
orderIdQuery.value.toLowerCase(),
);
final query = searchQuery.value.toLowerCase();
// Apply search filter: nama warga, id pesanan, atau asetId
final matchesSearch =
sewa.wargaNama.toLowerCase().contains(query) ||
sewa.id.toLowerCase().contains(query) ||
(sewa.asetId != null &&
sewa.asetId!.toLowerCase().contains(query));
// Apply status filter if not 'Semua'
final matchesStatus =
selectedStatusFilter.value == 'Semua' ||
sewa['status'] == selectedStatusFilter.value;
sewa.status.toUpperCase() ==
selectedStatusFilter.value.toUpperCase();
return matchesSearch && matchesOrderId && matchesStatus;
return matchesSearch && matchesStatus;
}).toList();
}
@ -68,100 +73,8 @@ class PetugasSewaController extends GetxController {
isLoading.value = true;
try {
// Simulate API call delay
await Future.delayed(const Duration(milliseconds: 800));
// Populate with mock data
sewaList.assignAll([
{
'id': '1',
'order_id': 'SWA-001',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-05',
'tanggal_selesai': '2025-02-10',
'total_biaya': 45000,
'status': 'Diterima',
'photo_url': 'https://example.com/photo1.jpg',
},
{
'id': '2',
'order_id': 'SWA-002',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-15',
'tanggal_selesai': '2025-02-20',
'total_biaya': 30000,
'status': 'Selesai',
'photo_url': 'https://example.com/photo2.jpg',
},
{
'id': '3',
'order_id': 'SWA-003',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-02-25',
'tanggal_selesai': '2025-03-01',
'total_biaya': 35000,
'status': 'Menunggu Pembayaran',
'photo_url': 'https://example.com/photo3.jpg',
},
{
'id': '4',
'order_id': 'SWA-004',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-05',
'tanggal_selesai': '2025-03-08',
'total_biaya': 20000,
'status': 'Periksa Pembayaran',
'photo_url': 'https://example.com/photo4.jpg',
},
{
'id': '5',
'order_id': 'SWA-005',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-12',
'tanggal_selesai': '2025-03-14',
'total_biaya': 15000,
'status': 'Dibatalkan',
'photo_url': 'https://example.com/photo5.jpg',
},
{
'id': '6',
'order_id': 'SWA-006',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-18',
'tanggal_selesai': '2025-03-20',
'total_biaya': 25000,
'status': 'Pembayaran Denda',
'photo_url': 'https://example.com/photo6.jpg',
},
{
'id': '7',
'order_id': 'SWA-007',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-03-25',
'tanggal_selesai': '2025-03-28',
'total_biaya': 40000,
'status': 'Periksa Denda',
'photo_url': 'https://example.com/photo7.jpg',
},
{
'id': '8',
'order_id': 'SWA-008',
'nama_warga': 'Sukimin',
'nama_aset': 'Mobil Pickup',
'tanggal_mulai': '2025-04-02',
'tanggal_selesai': '2025-04-05',
'total_biaya': 10000,
'status': 'Dikembalikan',
'photo_url': 'https://example.com/photo8.jpg',
},
]);
final data = await SewaService().fetchAllSewa();
sewaList.assignAll(data);
} catch (e) {
print('Error loading sewa data: $e');
} finally {
@ -196,10 +109,11 @@ class PetugasSewaController extends GetxController {
sewaList.where((sewa) {
bool matchesStatus =
selectedStatusFilter.value == 'Semua' ||
sewa['status'] == selectedStatusFilter.value;
sewa.status.toUpperCase() ==
selectedStatusFilter.value.toUpperCase();
bool matchesSearch =
searchQuery.value.isEmpty ||
sewa['nama_warga'].toLowerCase().contains(
sewa.wargaNama.toLowerCase().contains(
searchQuery.value.toLowerCase(),
);
return matchesStatus && matchesSearch;
@ -213,102 +127,367 @@ class PetugasSewaController extends GetxController {
// Get color based on status
Color getStatusColor(String status) {
switch (status) {
case 'Menunggu Pembayaran':
return Colors.orange;
case 'Periksa Pembayaran':
return Colors.amber.shade700;
case 'Diterima':
return Colors.blue;
case 'Pembayaran Denda':
return Colors.deepOrange;
case 'Periksa Denda':
return Colors.red.shade600;
case 'Dikembalikan':
return Colors.teal;
case 'Sedang Disewa':
switch (status.toUpperCase()) {
case 'MENUNGGU PEMBAYARAN':
return Colors.orangeAccent;
case 'PERIKSA PEMBAYARAN':
return Colors.amber;
case 'DITERIMA':
return Colors.blueAccent;
case 'AKTIF':
return Colors.green;
case 'Selesai':
case 'PEMBAYARAN DENDA':
return Colors.deepOrangeAccent;
case 'PERIKSA PEMBAYARAN DENDA':
return Colors.redAccent;
case 'DIKEMBALIKAN':
return Colors.teal;
case 'SELESAI':
return Colors.purple;
case 'Dibatalkan':
case 'DIBATALKAN':
return Colors.red;
default:
return Colors.grey;
}
}
// Get icon based on status
IconData getStatusIcon(String status) {
switch (status) {
case 'MENUNGGU PEMBAYARAN':
return Icons.payments_outlined;
case 'PERIKSA PEMBAYARAN':
return Icons.fact_check_outlined;
case 'DITERIMA':
return Icons.check_circle_outlined;
case 'AKTIF':
return Icons.play_circle_outline;
case 'PEMBYARAN DENDA':
return Icons.money_off_csred_outlined;
case 'PERIKSA PEMBAYARAN DENDA':
return Icons.assignment_late_outlined;
case 'DIKEMBALIKAN':
return Icons.assignment_return_outlined;
case 'SELESAI':
return Icons.task_alt_outlined;
case 'DIBATALKAN':
return Icons.cancel_outlined;
default:
return Icons.help_outline_rounded;
}
}
// Handle sewa approval (from "Periksa Pembayaran" to "Diterima")
void approveSewa(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]);
final currentStatus = sewa['status'];
if (currentStatus == 'Periksa Pembayaran') {
sewa['status'] = 'Diterima';
} else if (currentStatus == 'Periksa Denda') {
sewa['status'] = 'Selesai';
} else if (currentStatus == 'Menunggu Pembayaran') {
sewa['status'] = 'Periksa Pembayaran';
final sewa = sewaList[index];
final currentStatus = sewa.status;
String? newStatus;
if (currentStatus == 'PERIKSA PEMBAYARAN') {
newStatus = 'DITERIMA';
} else if (currentStatus == 'PERIKSA PEMBAYARAN DENDA') {
newStatus = 'SELESAI';
} else if (currentStatus == 'MENUNGGU PEMBAYARAN') {
newStatus = 'PERIKSA PEMBAYARAN';
}
if (newStatus != null) {
sewaList[index] = SewaModel(
id: sewa.id,
userId: sewa.userId,
status: newStatus,
waktuMulai: sewa.waktuMulai,
waktuSelesai: sewa.waktuSelesai,
tanggalPemesanan: sewa.tanggalPemesanan,
tipePesanan: sewa.tipePesanan,
kuantitas: sewa.kuantitas,
asetId: sewa.asetId,
asetNama: sewa.asetNama,
asetFoto: sewa.asetFoto,
paketId: sewa.paketId,
paketNama: sewa.paketNama,
paketFoto: sewa.paketFoto,
totalTagihan: sewa.totalTagihan,
wargaNama: sewa.wargaNama,
wargaNoHp: sewa.wargaNoHp,
wargaAvatar: sewa.wargaAvatar,
);
sewaList.refresh();
}
sewaList[index] = sewa;
sewaList.refresh();
}
}
// Handle sewa rejection or cancellation
void rejectSewa(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]);
sewa['status'] = 'Dibatalkan';
sewaList[index] = sewa;
final sewa = sewaList[index];
sewaList[index] = SewaModel(
id: sewa.id,
userId: sewa.userId,
status: 'Dibatalkan',
waktuMulai: sewa.waktuMulai,
waktuSelesai: sewa.waktuSelesai,
tanggalPemesanan: sewa.tanggalPemesanan,
tipePesanan: sewa.tipePesanan,
kuantitas: sewa.kuantitas,
asetId: sewa.asetId,
asetNama: sewa.asetNama,
asetFoto: sewa.asetFoto,
paketId: sewa.paketId,
paketNama: sewa.paketNama,
paketFoto: sewa.paketFoto,
totalTagihan: sewa.totalTagihan,
wargaNama: sewa.wargaNama,
wargaNoHp: sewa.wargaNoHp,
wargaAvatar: sewa.wargaAvatar,
);
sewaList.refresh();
}
}
// Request payment for penalty
void requestPenaltyPayment(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]);
sewa['status'] = 'Pembayaran Denda';
sewaList[index] = sewa;
final sewa = sewaList[index];
sewaList[index] = SewaModel(
id: sewa.id,
userId: sewa.userId,
status: 'Pembayaran Denda',
waktuMulai: sewa.waktuMulai,
waktuSelesai: sewa.waktuSelesai,
tanggalPemesanan: sewa.tanggalPemesanan,
tipePesanan: sewa.tipePesanan,
kuantitas: sewa.kuantitas,
asetId: sewa.asetId,
asetNama: sewa.asetNama,
asetFoto: sewa.asetFoto,
paketId: sewa.paketId,
paketNama: sewa.paketNama,
paketFoto: sewa.paketFoto,
totalTagihan: sewa.totalTagihan,
wargaNama: sewa.wargaNama,
wargaNoHp: sewa.wargaNoHp,
wargaAvatar: sewa.wargaAvatar,
);
sewaList.refresh();
}
}
// Mark penalty payment as requiring inspection
void markPenaltyForInspection(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]);
sewa['status'] = 'Periksa Denda';
sewaList[index] = sewa;
final sewa = sewaList[index];
sewaList[index] = SewaModel(
id: sewa.id,
userId: sewa.userId,
status: 'Periksa Denda',
waktuMulai: sewa.waktuMulai,
waktuSelesai: sewa.waktuSelesai,
tanggalPemesanan: sewa.tanggalPemesanan,
tipePesanan: sewa.tipePesanan,
kuantitas: sewa.kuantitas,
asetId: sewa.asetId,
asetNama: sewa.asetNama,
asetFoto: sewa.asetFoto,
paketId: sewa.paketId,
paketNama: sewa.paketNama,
paketFoto: sewa.paketFoto,
totalTagihan: sewa.totalTagihan,
wargaNama: sewa.wargaNama,
wargaNoHp: sewa.wargaNoHp,
wargaAvatar: sewa.wargaAvatar,
);
sewaList.refresh();
}
}
// Handle sewa completion
void completeSewa(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
void completeSewa(String id) async {
final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]);
sewa['status'] = 'Selesai';
sewaList[index] = sewa;
final sewa = sewaList[index];
sewaList[index] = SewaModel(
id: sewa.id,
userId: sewa.userId,
status: 'Selesai',
waktuMulai: sewa.waktuMulai,
waktuSelesai: sewa.waktuSelesai,
tanggalPemesanan: sewa.tanggalPemesanan,
tipePesanan: sewa.tipePesanan,
kuantitas: sewa.kuantitas,
asetId: sewa.asetId,
asetNama: sewa.asetNama,
asetFoto: sewa.asetFoto,
paketId: sewa.paketId,
paketNama: sewa.paketNama,
paketFoto: sewa.paketFoto,
totalTagihan: sewa.totalTagihan,
wargaNama: sewa.wargaNama,
wargaNoHp: sewa.wargaNoHp,
wargaAvatar: sewa.wargaAvatar,
);
sewaList.refresh();
// Update status in database
final asetProvider = Get.find<AsetProvider>();
await asetProvider.updateSewaAsetStatus(
sewaAsetId: id,
status: 'SELESAI',
);
}
}
// Mark rental as returned
void markAsReturned(String id) {
final index = sewaList.indexWhere((sewa) => sewa['id'] == id);
Future<void> markAsReturned(String id) async {
final index = sewaList.indexWhere((sewa) => sewa.id == id);
if (index != -1) {
final sewa = Map<String, dynamic>.from(sewaList[index]);
sewa['status'] = 'Dikembalikan';
sewaList[index] = sewa;
final sewa = sewaList[index];
sewaList[index] = SewaModel(
id: sewa.id,
userId: sewa.userId,
status: 'Dikembalikan',
waktuMulai: sewa.waktuMulai,
waktuSelesai: sewa.waktuSelesai,
tanggalPemesanan: sewa.tanggalPemesanan,
tipePesanan: sewa.tipePesanan,
kuantitas: sewa.kuantitas,
asetId: sewa.asetId,
asetNama: sewa.asetNama,
asetFoto: sewa.asetFoto,
paketId: sewa.paketId,
paketNama: sewa.paketNama,
paketFoto: sewa.paketFoto,
totalTagihan: sewa.totalTagihan,
wargaNama: sewa.wargaNama,
wargaNoHp: sewa.wargaNoHp,
wargaAvatar: sewa.wargaAvatar,
);
sewaList.refresh();
// Update status in database
final asetProvider = Get.find<AsetProvider>();
final result = await asetProvider.updateSewaAsetStatus(
sewaAsetId: id,
status: 'DIKEMBALIKAN',
);
if (!result) {
Get.snackbar(
'Gagal',
'Gagal mengubah status sewa di database',
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
}
// Ambil detail item paket (nama aset & kuantitas)
Future<List<Map<String, dynamic>>> getPaketItems(String paketId) async {
final asetProvider = Get.find<AsetProvider>();
debugPrint('[DEBUG] getPaketItems called with paketId: $paketId');
try {
final items = await asetProvider.getPaketItems(paketId);
debugPrint('[DEBUG] getPaketItems result for paketId $paketId:');
for (var item in items) {
debugPrint(' - item: ${item.toString()}');
}
return items;
} catch (e, stack) {
debugPrint('[ERROR] getPaketItems failed for paketId $paketId: $e');
debugPrint('[ERROR] Stacktrace: $stack');
return [];
}
}
RxBool getIsFullPayment(String sewaId) {
if (!isFullPaymentMap.containsKey(sewaId)) {
isFullPaymentMap[sewaId] = false.obs;
}
return isFullPaymentMap[sewaId]!;
}
TextEditingController getNominalController(String sewaId) {
if (!nominalControllerMap.containsKey(sewaId)) {
final controller = TextEditingController(text: '0');
nominalControllerMap[sewaId] = controller;
}
return nominalControllerMap[sewaId]!;
}
void setFullPayment(String sewaId, bool value, num totalTagihan) {
getIsFullPayment(sewaId).value = value;
if (value) {
getNominalController(sewaId).text = totalTagihan.toString();
}
}
RxString getPaymentMethod(String sewaId) {
if (!paymentMethodMap.containsKey(sewaId)) {
paymentMethodMap[sewaId] = 'Tunai'.obs;
}
return paymentMethodMap[sewaId]!;
}
void setPaymentMethod(String sewaId, String method) {
getPaymentMethod(sewaId).value = method;
}
Future<String?> getTagihanSewaIdBySewaAsetId(String sewaAsetId) async {
final asetProvider = Get.find<AsetProvider>();
final tagihan = await asetProvider.getTagihanSewa(sewaAsetId);
if (tagihan != null && tagihan['id'] != null) {
return tagihan['id'] as String;
}
return null;
}
Future<void> confirmPembayaranTagihan({
required String sewaAsetId,
required int nominal,
required String metodePembayaran,
}) async {
final tagihanSewaId = await getTagihanSewaIdBySewaAsetId(sewaAsetId);
if (tagihanSewaId == null) {
Get.snackbar(
'Gagal',
'Tagihan sewa tidak ditemukan',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
final asetProvider = Get.find<AsetProvider>();
// Cek status sewa_aset saat ini
final sewaAsetData = await asetProvider.getSewaAsetWithAsetData(sewaAsetId);
if (sewaAsetData != null &&
(sewaAsetData['status']?.toString()?.toUpperCase() ==
'PERIKSA PEMBAYARAN')) {
// Ubah status menjadi MENUNGGU PEMBAYARAN
await asetProvider.updateSewaAsetStatus(
sewaAsetId: sewaAsetId,
status: 'MENUNGGU PEMBAYARAN',
);
}
final result = await asetProvider.processPembayaranTagihan(
tagihanSewaId: tagihanSewaId,
nominal: nominal,
metodePembayaran: metodePembayaran,
);
if (result) {
Get.snackbar(
'Sukses',
'Pembayaran berhasil diproses',
backgroundColor: Colors.green,
colorText: Colors.white,
);
} else {
Get.snackbar(
'Gagal',
'Pembayaran gagal diproses',
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
}

View File

@ -1,7 +1,187 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:bumrent_app/app/data/models/aset_model.dart';
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
class PetugasTambahAsetController extends GetxController {
// Flag to check if in edit mode
final isEditing = false.obs;
String? assetId; // To store the ID of the asset being edited
@override
Future<void> onInit() async {
super.onInit();
try {
// Handle edit mode and load data if needed
final args = Get.arguments;
debugPrint('[DEBUG] PetugasTambahAsetController initialized with args: $args');
if (args != null && args is Map<String, dynamic>) {
isEditing.value = args['isEditing'] ?? false;
debugPrint('[DEBUG] isEditing set to: ${isEditing.value}');
if (isEditing.value) {
// Get asset ID from arguments
final assetId = args['assetId']?.toString() ?? '';
debugPrint('[DEBUG] Edit mode: Loading asset with ID: $assetId');
if (assetId.isNotEmpty) {
// Store the asset ID and load asset data
this.assetId = assetId;
debugPrint('[DEBUG] Asset ID set to: $assetId');
// Load asset data and await completion
await _loadAssetData(assetId);
} else {
debugPrint('[ERROR] Edit mode but no assetId provided in arguments');
Get.snackbar(
'Error',
'ID Aset tidak ditemukan',
snackPosition: SnackPosition.BOTTOM,
);
// Optionally navigate back if in edit mode without an ID
Future.delayed(Duration.zero, () => Get.back());
}
} else {
// Set default values for new asset
debugPrint('[DEBUG] Add new asset mode');
quantityController.text = '1';
unitOfMeasureController.text = 'Unit';
}
} else {
// Default values for new asset when no arguments are passed
debugPrint('[DEBUG] No arguments passed, defaulting to add new asset mode');
quantityController.text = '1';
unitOfMeasureController.text = 'Unit';
}
} catch (e, stackTrace) {
debugPrint('[ERROR] Error in onInit: $e');
debugPrint('Stack trace: $stackTrace');
// Ensure loading is set to false even if there's an error
isLoading.value = false;
Get.snackbar(
'Error',
'Terjadi kesalahan saat memuat data',
snackPosition: SnackPosition.BOTTOM,
);
}
// Listen to field changes for validation
nameController.addListener(validateForm);
descriptionController.addListener(validateForm);
quantityController.addListener(validateForm);
pricePerHourController.addListener(validateForm);
pricePerDayController.addListener(validateForm);
}
final AsetProvider _asetProvider = Get.find<AsetProvider>();
final isLoading = false.obs;
Future<void> _loadAssetData(String assetId) async {
try {
isLoading.value = true;
debugPrint('[DEBUG] Fetching asset data for ID: $assetId');
// Fetch asset data from Supabase
final aset = await _asetProvider.getAsetById(assetId);
if (aset == null) {
throw Exception('Aset tidak ditemukan');
}
debugPrint('[DEBUG] Successfully fetched asset data: ${aset.toJson()}');
// Populate form fields with the fetched data
nameController.text = aset.nama ?? '';
descriptionController.text = aset.deskripsi ?? '';
quantityController.text = (aset.kuantitas ?? 1).toString();
// Ensure the status matches one of the available options exactly
final status = aset.status?.toLowerCase() ?? 'tersedia';
if (status == 'tersedia') {
selectedStatus.value = 'Tersedia';
} else if (status == 'pemeliharaan') {
selectedStatus.value = 'Pemeliharaan';
} else {
// Default to 'Tersedia' if status is not recognized
selectedStatus.value = 'Tersedia';
}
// Handle time options and pricing
if (aset.satuanWaktuSewa != null && aset.satuanWaktuSewa!.isNotEmpty) {
// Reset time options
timeOptions.forEach((key, value) => value.value = false);
// Process each satuan waktu sewa
for (var sws in aset.satuanWaktuSewa) {
final satuan = sws['nama_satuan_waktu']?.toString().toLowerCase() ?? '';
final harga = sws['harga'] as int? ?? 0;
final maksimalWaktu = sws['maksimal_waktu'] as int? ?? 24;
if (satuan.contains('jam')) {
timeOptions['Per Jam']?.value = true;
pricePerHourController.text = harga.toString();
maxHourController.text = maksimalWaktu.toString();
} else if (satuan.contains('hari')) {
timeOptions['Per Hari']?.value = true;
pricePerDayController.text = harga.toString();
maxDayController.text = maksimalWaktu.toString();
}
}
}
// Clear existing images
selectedImages.clear();
networkImageUrls.clear();
// Get all image URLs from the model
final allImageUrls = aset.imageUrls.toList();
// If no imageUrls but has imageUrl, use that as fallback (backward compatibility)
if (allImageUrls.isEmpty && aset.imageUrl != null && aset.imageUrl!.isNotEmpty) {
allImageUrls.add(aset.imageUrl!);
}
// Add all images to the lists
for (final imageUrl in allImageUrls) {
if (imageUrl != null && imageUrl.isNotEmpty) {
try {
// For network images, we'll store the URL in networkImageUrls
// and create a dummy XFile with the URL as path for backward compatibility
final dummyFile = XFile(imageUrl);
selectedImages.add(dummyFile);
networkImageUrls.add(imageUrl);
debugPrint('Added network image: $imageUrl');
} catch (e) {
debugPrint('Error adding network image: $e');
}
}
}
debugPrint('Total ${networkImageUrls.length} images loaded for asset $assetId');
debugPrint('[DEBUG] Successfully loaded asset data for ID: $assetId');
} catch (e, stackTrace) {
debugPrint('[ERROR] Failed to load asset data: $e');
debugPrint('Stack trace: $stackTrace');
Get.snackbar(
'Error',
'Gagal memuat data aset: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
// Optionally navigate back if there's an error
Future.delayed(const Duration(seconds: 2), () => Get.back());
} finally {
isLoading.value = false;
}
}
// Form controllers
final nameController = TextEditingController();
final descriptionController = TextEditingController();
@ -23,27 +203,17 @@ class PetugasTambahAsetController extends GetxController {
final categoryOptions = ['Sewa', 'Langganan'];
final statusOptions = ['Tersedia', 'Pemeliharaan'];
// Images
final selectedImages = <String>[].obs;
// List to store selected images
final RxList<XFile> selectedImages = <XFile>[].obs;
// List to store network image URLs
final RxList<String> networkImageUrls = <String>[].obs;
final _picker = ImagePicker();
// Form validation
final isFormValid = false.obs;
final isSubmitting = false.obs;
@override
void onInit() {
super.onInit();
// Set default values
quantityController.text = '1';
unitOfMeasureController.text = 'Unit';
// Listen to field changes for validation
nameController.addListener(validateForm);
descriptionController.addListener(validateForm);
quantityController.addListener(validateForm);
pricePerHourController.addListener(validateForm);
pricePerDayController.addListener(validateForm);
}
@override
void onClose() {
@ -85,21 +255,144 @@ class PetugasTambahAsetController extends GetxController {
if (!anySelected) {
timeOptions[option]?.value = true;
}
validateForm();
}
// Add image to the list (in a real app, this would handle file upload)
void addImage(String imagePath) {
selectedImages.add(imagePath);
validateForm();
// Create a new asset in Supabase
Future<String?> _createAsset(
Map<String, dynamic> assetData,
List<Map<String, dynamic>> satuanWaktuSewa,
) async {
try {
// Create the asset in the 'aset' table
final response = await _asetProvider.createAset(assetData);
if (response == null || response['id'] == null) {
debugPrint('❌ Failed to create asset: No response or ID from server');
return null;
}
final String assetId = response['id'].toString();
debugPrint('✅ Asset created with ID: $assetId');
// Add satuan waktu sewa
for (var sws in satuanWaktuSewa) {
final success = await _asetProvider.addSatuanWaktuSewa(
asetId: assetId,
satuanWaktu: sws['satuan_waktu'],
harga: sws['harga'],
maksimalWaktu: sws['maksimal_waktu'],
);
if (!success) {
debugPrint('❌ Failed to add satuan waktu sewa: $sws');
}
}
return assetId;
} catch (e, stackTrace) {
debugPrint('❌ Error creating asset: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
// Remove image from the list
// Update an existing asset in Supabase
Future<bool> _updateAsset(
String assetId,
Map<String, dynamic> assetData,
List<Map<String, dynamic>> satuanWaktuSewa,
) async {
try {
debugPrint('\n🔄 Starting update for asset ID: $assetId');
// 1. Extract and remove foto_aset from assetData as it's not in the aset table
final fotoAsetUrl = assetData['foto_aset'];
assetData.remove('foto_aset');
debugPrint('📝 Asset data prepared for update (without foto_aset)');
// 2. Update the main asset data (without foto_aset)
debugPrint('🔄 Updating main asset data...');
final success = await _asetProvider.updateAset(assetId, assetData);
if (!success) {
debugPrint('❌ Failed to update asset with ID: $assetId');
return false;
}
debugPrint('✅ Successfully updated main asset data');
// 3. Update satuan waktu sewa
debugPrint('\n🔄 Updating rental time units...');
// First, delete existing satuan waktu sewa
await _asetProvider.deleteSatuanWaktuSewaByAsetId(assetId);
// Then add the new ones
for (var sws in satuanWaktuSewa) {
debugPrint(' - Adding: ${sws['satuan_waktu']} (${sws['harga']} IDR)');
await _asetProvider.addSatuanWaktuSewa(
asetId: assetId,
satuanWaktu: sws['satuan_waktu'],
harga: sws['harga'] as int,
maksimalWaktu: sws['maksimal_waktu'] as int,
);
}
debugPrint('✅ Successfully updated rental time units');
// 4. Update photos in the foto_aset table if any exist
if (selectedImages.isNotEmpty || networkImageUrls.isNotEmpty) {
// Combine network URLs and local file paths
final List<String> allImageUrls = [
...networkImageUrls,
...selectedImages.map((file) => file.path),
];
debugPrint('\n🖼️ Processing photos for asset $assetId');
debugPrint(' - Network URLs: ${networkImageUrls.length}');
debugPrint(' - Local files: ${selectedImages.length}');
debugPrint(' - Total unique photos: ${allImageUrls.toSet().length} (before deduplication)');
try {
// Use updateFotoAset which handles both uploading new photos and updating the database
final photoSuccess = await _asetProvider.updateFotoAset(
asetId: assetId,
fotoUrls: allImageUrls,
);
if (!photoSuccess) {
debugPrint('⚠️ Some photos might not have been updated for asset $assetId');
// We don't fail the whole update if photo update fails
// as the main asset data has been saved successfully
} else {
debugPrint('✅ Successfully updated photos for asset $assetId');
}
} catch (e, stackTrace) {
debugPrint('❌ Error updating photos: $e');
debugPrint('Stack trace: $stackTrace');
// Continue with the update even if photo update fails
}
} else {
debugPrint(' No photos to update');
}
debugPrint('\n✅ Asset update completed successfully for ID: $assetId');
return true;
} catch (e, stackTrace) {
debugPrint('❌ Error updating asset: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
// Remove an image from the selected images list
void removeImage(int index) {
if (index >= 0 && index < selectedImages.length) {
// Remove from both lists if they have an entry at this index
if (index < networkImageUrls.length) {
networkImageUrls.removeAt(index);
}
selectedImages.removeAt(index);
validateForm();
}
}
@ -133,62 +426,130 @@ class PetugasTambahAsetController extends GetxController {
basicValid && perHourValid && perDayValid && anyTimeOptionSelected;
}
// Submit form and save asset
// Submit form and save or update asset
Future<void> saveAsset() async {
if (!isFormValid.value) return;
isSubmitting.value = true;
try {
// In a real app, this would make an API call to save the asset
await Future.delayed(const Duration(seconds: 1)); // Mock API call
// Prepare asset data
final assetData = {
// Prepare the basic asset data
final Map<String, dynamic> assetData = {
'nama': nameController.text,
'deskripsi': descriptionController.text,
'kategori': selectedCategory.value,
'kategori': 'sewa', // Default to 'sewa' category
'status': selectedStatus.value,
'kuantitas': int.parse(quantityController.text),
'satuan_ukur': unitOfMeasureController.text,
'opsi_waktu_sewa':
timeOptions.entries
.where((entry) => entry.value.value)
.map((entry) => entry.key)
.toList(),
'harga_per_jam':
timeOptions['Per Jam']!.value
? int.parse(pricePerHourController.text)
: null,
'max_jam':
timeOptions['Per Jam']!.value && maxHourController.text.isNotEmpty
? int.parse(maxHourController.text)
: null,
'harga_per_hari':
timeOptions['Per Hari']!.value
? int.parse(pricePerDayController.text)
: null,
'max_hari':
timeOptions['Per Hari']!.value && maxDayController.text.isNotEmpty
? int.parse(maxDayController.text)
: null,
'gambar': selectedImages,
'satuan_ukur': 'unit', // Default unit of measure
};
// Log the data (in a real app, this would be sent to an API)
print('Asset data: $assetData');
// Handle time options and pricing
final List<Map<String, dynamic>> satuanWaktuSewa = [];
if (timeOptions['Per Jam']?.value == true) {
final hargaPerJam = int.tryParse(pricePerHourController.text) ?? 0;
final maxJam = int.tryParse(maxHourController.text) ?? 24;
if (hargaPerJam <= 0) {
throw Exception('Harga per jam harus lebih dari 0');
}
satuanWaktuSewa.add({
'satuan_waktu': 'jam',
'harga': hargaPerJam,
'maksimal_waktu': maxJam,
});
}
if (timeOptions['Per Hari']?.value == true) {
final hargaPerHari = int.tryParse(pricePerDayController.text) ?? 0;
final maxHari = int.tryParse(maxDayController.text) ?? 30;
if (hargaPerHari <= 0) {
throw Exception('Harga per hari harus lebih dari 0');
}
satuanWaktuSewa.add({
'satuan_waktu': 'hari',
'harga': hargaPerHari,
'maksimal_waktu': maxHari,
});
}
// Return to the asset list page
Get.back();
// Validate that at least one time option is selected
if (satuanWaktuSewa.isEmpty) {
throw Exception('Pilih setidaknya satu opsi waktu sewa (jam/hari)');
}
// Show success message
Get.snackbar(
'Berhasil',
'Aset berhasil ditambahkan',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
// Handle image uploads
List<String> imageUrls = [];
if (networkImageUrls.isNotEmpty) {
// Use existing network URLs
imageUrls = List.from(networkImageUrls);
} else if (selectedImages.isNotEmpty) {
// For local files, we'll upload them to Supabase Storage
// Store the file paths for now, they'll be uploaded in the provider
imageUrls = selectedImages.map((file) => file.path).toList();
debugPrint('Found ${imageUrls.length} local images to upload');
} else if (!isEditing.value) {
// For new assets, require at least one image
throw Exception('Harap unggah setidaknya satu gambar');
}
// Ensure at least one image is provided for new assets
if (imageUrls.isEmpty && !isEditing.value) {
throw Exception('Harap unggah setidaknya satu gambar');
}
// Create or update the asset
bool success;
String? createdAssetId;
if (isEditing.value && (assetId?.isNotEmpty ?? false)) {
// Update existing asset
debugPrint('🔄 Updating asset with ID: $assetId');
success = await _updateAsset(assetId!, assetData, satuanWaktuSewa);
// Update all photos if we have any
if (success && imageUrls.isNotEmpty) {
await _asetProvider.updateFotoAset(
asetId: assetId!,
fotoUrls: imageUrls,
);
}
} else {
// Create new asset
debugPrint('🔄 Creating new asset');
createdAssetId = await _createAsset(assetData, satuanWaktuSewa);
success = createdAssetId != null;
// Add all photos for new asset
if (success && createdAssetId != null && imageUrls.isNotEmpty) {
await _asetProvider.updateFotoAset(
asetId: createdAssetId,
fotoUrls: imageUrls,
);
}
}
if (success) {
// Show success message
Get.snackbar(
'Sukses',
isEditing.value ? 'Aset berhasil diperbarui' : 'Aset berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
// Navigate back with success after a short delay
await Future.delayed(const Duration(seconds: 1));
Get.back(result: true);
} else {
throw Exception('Gagal menyimpan aset');
}
} catch (e) {
// Show error message
Get.snackbar(
@ -203,8 +564,68 @@ class PetugasTambahAsetController extends GetxController {
}
}
// Example method to upload images (to be implemented with your backend)
// Future<List<String>> _uploadImages(List<XFile> images) async {
// List<String> urls = [];
// for (var image in images) {
// // Upload image to your server and get the URL
// // final url = await yourApiService.uploadImage(File(image.path));
// // urls.add(url);
// urls.add('https://example.com/path/to/uploaded/image.jpg'); // Mock URL
// }
// return urls;
// }
// Pick image from camera
Future<void> pickImageFromCamera() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
maxWidth: 1024,
maxHeight: 1024,
);
if (image != null) {
selectedImages.add(image);
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal mengambil gambar dari kamera: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// Pick image from gallery
Future<void> pickImageFromGallery() async {
try {
final List<XFile>? images = await _picker.pickMultiImage(
imageQuality: 80,
maxWidth: 1024,
maxHeight: 1024,
);
if (images != null && images.isNotEmpty) {
selectedImages.addAll(images);
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal memilih gambar dari galeri: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
// For demonstration purposes: add sample image
void addSampleImage() {
addImage('assets/images/sample_asset_${selectedImages.length + 1}.jpg');
// In a real app, this would open the image picker
selectedImages.add(XFile('assets/images/sample_asset_${selectedImages.length + 1}.jpg'));
validateForm();
}
}

View File

@ -1,5 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:bumrent_app/app/data/models/paket_model.dart';
import 'package:image_picker/image_picker.dart';
import 'package:bumrent_app/app/data/providers/aset_provider.dart';
import 'dart:io';
import 'package:uuid/uuid.dart';
class PetugasTambahPaketController extends GetxController {
// Form controllers
@ -10,14 +16,14 @@ class PetugasTambahPaketController extends GetxController {
// Dropdown and toggle values
final selectedCategory = 'Bulanan'.obs;
final selectedStatus = 'Aktif'.obs;
final selectedStatus = 'Tersedia'.obs;
// Category options
final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis'];
final statusOptions = ['Aktif', 'Nonaktif'];
final statusOptions = ['Tersedia', 'Pemeliharaan'];
// Images
final selectedImages = <String>[].obs;
final selectedImages = <dynamic>[].obs;
// For package name and description
final packageNameController = TextEditingController();
@ -31,21 +37,85 @@ class PetugasTambahPaketController extends GetxController {
// For asset selection
final RxList<Map<String, dynamic>> availableAssets =
<Map<String, dynamic>>[].obs;
final Rx<int?> selectedAsset = Rx<int?>(null);
final Rx<String?> selectedAsset = Rx<String?>(null);
final RxBool isLoadingAssets = false.obs;
// Form validation
final isFormValid = false.obs;
final isSubmitting = false.obs;
// New RxBool for editing
final isEditing = false.obs;
final timeOptions = {'Per Jam': true.obs, 'Per Hari': false.obs};
final pricePerHourController = TextEditingController();
final maxHourController = TextEditingController();
final pricePerDayController = TextEditingController();
final maxDayController = TextEditingController();
final _picker = ImagePicker();
final isFormChanged = false.obs;
Map<String, dynamic> initialFormData = {};
final AsetProvider _asetProvider = Get.put(AsetProvider());
@override
void onInit() {
super.onInit();
// Ambil flag isEditing dari arguments
isEditing.value =
Get.arguments != null && Get.arguments['isEditing'] == true;
if (isEditing.value) {
final paketArg = Get.arguments['paket'];
String? paketId;
if (paketArg != null) {
if (paketArg is Map && paketArg['id'] != null) {
paketId = paketArg['id'].toString();
} else if (paketArg is PaketModel && paketArg.id != null) {
paketId = paketArg.id.toString();
}
}
if (paketId != null) {
fetchPaketDetail(paketId);
}
}
// Listen to field changes for validation
nameController.addListener(validateForm);
descriptionController.addListener(validateForm);
priceController.addListener(validateForm);
nameController.addListener(() {
validateForm();
checkFormChanged();
});
descriptionController.addListener(() {
validateForm();
checkFormChanged();
});
priceController.addListener(() {
validateForm();
checkFormChanged();
});
itemQuantityController.addListener(() {
validateForm();
checkFormChanged();
});
pricePerHourController.addListener(() {
validateForm();
checkFormChanged();
});
maxHourController.addListener(() {
validateForm();
checkFormChanged();
});
pricePerDayController.addListener(() {
validateForm();
checkFormChanged();
});
maxDayController.addListener(() {
validateForm();
checkFormChanged();
});
// Load available assets when the controller initializes
fetchAvailableAssets();
@ -61,6 +131,10 @@ class PetugasTambahPaketController extends GetxController {
packageNameController.dispose();
packageDescriptionController.dispose();
packagePriceController.dispose();
pricePerHourController.dispose();
maxHourController.dispose();
pricePerDayController.dispose();
maxDayController.dispose();
super.onClose();
}
@ -68,18 +142,21 @@ class PetugasTambahPaketController extends GetxController {
void setCategory(String category) {
selectedCategory.value = category;
validateForm();
checkFormChanged();
}
// Change selected status
void setStatus(String status) {
selectedStatus.value = status;
validateForm();
checkFormChanged();
}
// Add image to the list (in a real app, this would handle file upload)
void addImage(String imagePath) {
selectedImages.add(imagePath);
validateForm();
checkFormChanged();
}
// Remove image from the list
@ -87,34 +164,43 @@ class PetugasTambahPaketController extends GetxController {
if (index >= 0 && index < selectedImages.length) {
selectedImages.removeAt(index);
validateForm();
checkFormChanged();
}
}
// Fetch available assets from the API or local data
void fetchAvailableAssets() {
// Fetch available assets from Supabase and filter out already selected ones
Future<void> fetchAvailableAssets() async {
isLoadingAssets.value = true;
// This is a mock implementation - replace with actual API call
Future.delayed(const Duration(seconds: 1), () {
availableAssets.value = [
{'id': 1, 'nama': 'Laptop Dell XPS', 'stok': 5},
{'id': 2, 'nama': 'Proyektor Epson', 'stok': 3},
{'id': 3, 'nama': 'Meja Kantor', 'stok': 10},
{'id': 4, 'nama': 'Kursi Ergonomis', 'stok': 15},
{'id': 5, 'nama': 'Printer HP LaserJet', 'stok': 2},
{'id': 6, 'nama': 'AC Panasonic 1PK', 'stok': 8},
];
try {
final allAssets = await _asetProvider.getSewaAsets();
final selectedAsetIds =
packageItems.map((item) => item['asetId'].toString()).toSet();
// Only show assets not yet selected
availableAssets.value =
allAssets
.where((aset) => !selectedAsetIds.contains(aset.id))
.map(
(aset) => {
'id': aset.id,
'nama': aset.nama,
'stok': aset.kuantitas,
},
)
.toList();
} catch (e) {
availableAssets.value = [];
} finally {
isLoadingAssets.value = false;
});
}
}
// Set the selected asset
void setSelectedAsset(int? assetId) {
void setSelectedAsset(String? assetId) {
selectedAsset.value = assetId;
}
// Get remaining stock for an asset (considering current selections)
int getRemainingStock(int assetId) {
int getRemainingStock(String assetId) {
// Find the asset in available assets
final asset = availableAssets.firstWhere(
(item) => item['id'] == assetId,
@ -129,7 +215,7 @@ class PetugasTambahPaketController extends GetxController {
// Calculate how many of this asset are already in the package
int alreadySelected = 0;
for (var item in packageItems) {
if (item['asetId'] == assetId) {
if (item['asetId'].toString() == assetId) {
alreadySelected += item['jumlah'] as int;
}
}
@ -204,6 +290,8 @@ class PetugasTambahPaketController extends GetxController {
backgroundColor: Colors.green,
colorText: Colors.white,
);
checkFormChanged();
}
// Update an existing package item
@ -301,11 +389,16 @@ class PetugasTambahPaketController extends GetxController {
backgroundColor: Colors.green,
colorText: Colors.white,
);
checkFormChanged();
}
// Remove an item from the package
void removeItem(int index) {
packageItems.removeAt(index);
if (index >= 0 && index < packageItems.length) {
packageItems.removeAt(index);
checkFormChanged();
}
Get.snackbar(
'Dihapus',
'Item berhasil dihapus dari paket',
@ -319,10 +412,7 @@ class PetugasTambahPaketController extends GetxController {
void validateForm() {
// Basic validation
bool basicValid =
nameController.text.isNotEmpty &&
descriptionController.text.isNotEmpty &&
priceController.text.isNotEmpty &&
int.tryParse(priceController.text) != null;
nameController.text.isNotEmpty && descriptionController.text.isNotEmpty;
// Package should have at least one item
bool hasItems = packageItems.isNotEmpty;
@ -337,39 +427,204 @@ class PetugasTambahPaketController extends GetxController {
isSubmitting.value = true;
try {
// In a real app, this would make an API call to save the package
await Future.delayed(const Duration(seconds: 1)); // Mock API call
final supabase = Supabase.instance.client;
if (isEditing.value) {
// --- UPDATE LOGIC ---
final paketArg = Get.arguments['paket'];
final String paketId =
paketArg is Map && paketArg['id'] != null
? paketArg['id'].toString()
: (paketArg is PaketModel && paketArg.id != null
? paketArg.id.toString()
: '');
if (paketId.isEmpty) throw Exception('ID paket tidak ditemukan');
// Prepare package data
final paketData = {
'nama': nameController.text,
'deskripsi': descriptionController.text,
'kategori': selectedCategory.value,
'status': selectedStatus.value == 'Aktif',
'harga': int.parse(priceController.text),
'gambar': selectedImages,
'items': packageItems,
};
// 1. Update data utama paket
await supabase
.from('paket')
.update({
'nama': nameController.text,
'deskripsi': descriptionController.text,
'status': selectedStatus.value.toLowerCase(),
})
.eq('id', paketId);
// Log the data (in a real app, this would be sent to an API)
print('Package data: $paketData');
// 2. Update paket_item: hapus semua, insert ulang
await supabase.from('paket_item').delete().eq('paket_id', paketId);
for (var item in packageItems) {
await supabase.from('paket_item').insert({
'paket_id': paketId,
'aset_id': item['asetId'],
'kuantitas': item['jumlah'],
});
}
// Return to the package list page
Get.back();
// 3. Update satuan_waktu_sewa: hapus semua, insert ulang
await supabase
.from('satuan_waktu_sewa')
.delete()
.eq('paket_id', paketId);
// Fetch satuan_waktu UUIDs
final satuanWaktuList = await supabase
.from('satuan_waktu')
.select('id, nama_satuan_waktu');
String? jamId;
String? hariId;
for (var sw in satuanWaktuList) {
final nama = (sw['nama_satuan_waktu'] ?? '').toString().toLowerCase();
if (nama.contains('jam')) jamId = sw['id'];
if (nama.contains('hari')) hariId = sw['id'];
}
if (timeOptions['Per Jam']?.value == true && jamId != null) {
await supabase.from('satuan_waktu_sewa').insert({
'paket_id': paketId,
'satuan_waktu_id': jamId,
'harga': int.tryParse(pricePerHourController.text) ?? 0,
'maksimal_waktu': int.tryParse(maxHourController.text) ?? 0,
});
}
if (timeOptions['Per Hari']?.value == true && hariId != null) {
await supabase.from('satuan_waktu_sewa').insert({
'paket_id': paketId,
'satuan_waktu_id': hariId,
'harga': int.tryParse(pricePerDayController.text) ?? 0,
'maksimal_waktu': int.tryParse(maxDayController.text) ?? 0,
});
}
// Show success message
Get.snackbar(
'Berhasil',
'Paket berhasil ditambahkan',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
// 4. Update foto_aset
// a. Ambil foto lama dari DB
final oldPhotos = await supabase
.from('foto_aset')
.select('foto_aset')
.eq('id_paket', paketId);
final oldPhotoUrls =
oldPhotos
.map((e) => e['foto_aset']?.toString())
.whereType<String>()
.toSet();
final newPhotoUrls =
selectedImages
.map((img) => img is String ? img : (img.path ?? ''))
.where((e) => e.isNotEmpty)
.toSet();
// b. Hapus foto yang dihapus user (dari DB dan storage)
final removedPhotos = oldPhotoUrls.difference(newPhotoUrls);
for (final url in removedPhotos) {
await supabase
.from('foto_aset')
.delete()
.eq('foto_aset', url)
.eq('id_paket', paketId);
await _asetProvider.deleteFileFromStorage(url);
}
// c. Tambah foto baru (upload jika perlu, insert ke DB)
for (final img in selectedImages) {
String url = '';
if (img is String && img.startsWith('http')) {
url = img;
} else if (img is XFile) {
final uploaded = await _asetProvider.uploadFileToStorage(
File(img.path),
);
if (uploaded != null) url = uploaded;
}
if (url.isNotEmpty && !oldPhotoUrls.contains(url)) {
await supabase.from('foto_aset').insert({
'id_paket': paketId,
'foto_aset': url,
});
}
}
// Sukses
Get.back();
Get.snackbar(
'Berhasil',
'Paket berhasil diperbarui',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} else {
// --- ADD LOGIC ---
final uuid = Uuid();
final String paketId = uuid.v4();
// 1. Insert ke tabel paket
await supabase.from('paket').insert({
'id': paketId,
'nama': nameController.text,
'deskripsi': descriptionController.text,
'status': selectedStatus.value.toLowerCase(),
});
// 2. Insert ke paket_item
for (var item in packageItems) {
await supabase.from('paket_item').insert({
'paket_id': paketId,
'aset_id': item['asetId'],
'kuantitas': item['jumlah'],
});
}
// 3. Insert ke satuan_waktu_sewa (ambil UUID satuan waktu)
final satuanWaktuList = await supabase
.from('satuan_waktu')
.select('id, nama_satuan_waktu');
String? jamId;
String? hariId;
for (var sw in satuanWaktuList) {
final nama = (sw['nama_satuan_waktu'] ?? '').toString().toLowerCase();
if (nama.contains('jam')) jamId = sw['id'];
if (nama.contains('hari')) hariId = sw['id'];
}
if (timeOptions['Per Jam']?.value == true && jamId != null) {
await supabase.from('satuan_waktu_sewa').insert({
'paket_id': paketId,
'satuan_waktu_id': jamId,
'harga': int.tryParse(pricePerHourController.text) ?? 0,
'maksimal_waktu': int.tryParse(maxHourController.text) ?? 0,
});
}
if (timeOptions['Per Hari']?.value == true && hariId != null) {
await supabase.from('satuan_waktu_sewa').insert({
'paket_id': paketId,
'satuan_waktu_id': hariId,
'harga': int.tryParse(pricePerDayController.text) ?? 0,
'maksimal_waktu': int.tryParse(maxDayController.text) ?? 0,
});
}
// 4. Insert ke foto_aset (upload jika perlu)
for (final img in selectedImages) {
String url = '';
if (img is String && img.startsWith('http')) {
url = img;
} else if (img is XFile) {
final uploaded = await _asetProvider.uploadFileToStorage(
File(img.path),
);
if (uploaded != null) url = uploaded;
}
if (url.isNotEmpty) {
await supabase.from('foto_aset').insert({
'id_paket': paketId,
'foto_aset': url,
});
}
}
// Sukses
Get.back();
Get.snackbar(
'Berhasil',
'Paket berhasil ditambahkan',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
}
} catch (e) {
// Show error message
Get.snackbar(
'Gagal',
'Terjadi kesalahan: ${e.toString()}',
'Terjadi kesalahan: \\${e.toString()}',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
@ -390,4 +645,215 @@ class PetugasTambahPaketController extends GetxController {
selectedImages.add('https://example.com/sample_image.jpg');
validateForm();
}
void toggleTimeOption(String option) {
timeOptions[option]?.value = !(timeOptions[option]?.value ?? false);
// Ensure at least one option is selected
bool anySelected = false;
timeOptions.forEach((key, value) {
if (value.value) anySelected = true;
});
if (!anySelected) {
timeOptions[option]?.value = true;
}
validateForm();
checkFormChanged();
}
Future<void> fetchPaketDetail(String paketId) async {
try {
debugPrint('[DEBUG] Fetching paket detail for id: $paketId');
final supabase = Supabase.instance.client;
// 1) Ambil data paket utama
final paketData =
await supabase
.from('paket')
.select('id, nama, deskripsi, status')
.eq('id', paketId)
.single();
debugPrint('[DEBUG] Paket data: ' + paketData.toString());
// 2) Ambil paket_item
final paketItemData = await supabase
.from('paket_item')
.select('id, paket_id, aset_id, kuantitas')
.eq('paket_id', paketId);
debugPrint('[DEBUG] Paket item data: ' + paketItemData.toString());
// 3) Ambil satuan_waktu_sewa
final swsData = await supabase
.from('satuan_waktu_sewa')
.select('id, paket_id, satuan_waktu_id, harga, maksimal_waktu')
.eq('paket_id', paketId);
debugPrint('[DEBUG] Satuan waktu sewa data: ' + swsData.toString());
// 4) Ambil semua satuan_waktu_id dari swsData
final swIds = swsData.map((e) => e['satuan_waktu_id']).toSet().toList();
final swData =
swIds.isNotEmpty
? await supabase
.from('satuan_waktu')
.select('id, nama_satuan_waktu')
.inFilter('id', swIds)
: [];
debugPrint('[DEBUG] Satuan waktu data: ' + swData.toString());
final Map satuanWaktuMap = {
for (var sw in swData) sw['id']: sw['nama_satuan_waktu'],
};
// 5) Ambil foto_aset
final fotoData = await supabase
.from('foto_aset')
.select('id_paket, foto_aset')
.eq('id_paket', paketId);
debugPrint('[DEBUG] Foto aset data: ' + fotoData.toString());
// 6) Kumpulkan semua aset_id dari paketItemData
final asetIds = paketItemData.map((e) => e['aset_id']).toSet().toList();
final asetData =
asetIds.isNotEmpty
? await supabase
.from('aset')
.select('id, nama, kuantitas')
.inFilter('id', asetIds)
: [];
debugPrint('[DEBUG] Aset data: ' + asetData.toString());
final Map asetMap = {for (var a in asetData) a['id']: a};
// Prefill field controller
nameController.text = paketData['nama']?.toString() ?? '';
descriptionController.text = paketData['deskripsi']?.toString() ?? '';
// Status mapping
final statusDb =
(paketData['status']?.toString().toLowerCase() ?? 'tersedia');
selectedStatus.value =
statusDb == 'pemeliharaan' ? 'Pemeliharaan' : 'Tersedia';
// Foto
selectedImages.clear();
if (fotoData.isNotEmpty) {
for (var foto in fotoData) {
final url = foto['foto_aset']?.toString();
if (url != null && url.isNotEmpty) {
selectedImages.add(url);
}
}
}
// Item paket
packageItems.clear();
for (var item in paketItemData) {
final aset = asetMap[item['aset_id']];
packageItems.add({
'asetId': item['aset_id'],
'nama': aset != null ? aset['nama'] : '',
'jumlah': item['kuantitas'],
'stok': aset != null ? aset['kuantitas'] : 0,
});
}
// Opsi waktu & harga sewa
// Reset
timeOptions['Per Jam']?.value = false;
timeOptions['Per Hari']?.value = false;
pricePerHourController.clear();
maxHourController.clear();
pricePerDayController.clear();
maxDayController.clear();
for (var sws in swsData) {
final satuanNama =
satuanWaktuMap[sws['satuan_waktu_id']]?.toString().toLowerCase() ??
'';
if (satuanNama.contains('jam')) {
timeOptions['Per Jam']?.value = true;
pricePerHourController.text = (sws['harga'] ?? '').toString();
maxHourController.text = (sws['maksimal_waktu'] ?? '').toString();
} else if (satuanNama.contains('hari')) {
timeOptions['Per Hari']?.value = true;
pricePerDayController.text = (sws['harga'] ?? '').toString();
maxDayController.text = (sws['maksimal_waktu'] ?? '').toString();
}
}
// Simpan snapshot initialFormData setelah prefill
initialFormData = {
'nama': nameController.text,
'deskripsi': descriptionController.text,
'status': selectedStatus.value,
'images': List.from(selectedImages),
'items': List.from(packageItems),
'perJam': timeOptions['Per Jam']?.value ?? false,
'perHari': timeOptions['Per Hari']?.value ?? false,
'hargaJam': pricePerHourController.text,
'maxJam': maxHourController.text,
'hargaHari': pricePerDayController.text,
'maxHari': maxDayController.text,
};
isFormChanged.value = false;
} catch (e, st) {
debugPrint('[ERROR] Gagal fetch paket detail: $e');
}
}
Future<void> pickImageFromCamera() async {
try {
final XFile? image = await _picker.pickImage(
source: ImageSource.camera,
imageQuality: 80,
maxWidth: 1024,
maxHeight: 1024,
);
if (image != null) {
selectedImages.add(image);
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal mengambil gambar dari kamera: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
Future<void> pickImageFromGallery() async {
try {
final List<XFile>? images = await _picker.pickMultiImage(
imageQuality: 80,
maxWidth: 1024,
maxHeight: 1024,
);
if (images != null && images.isNotEmpty) {
for (final img in images) {
selectedImages.add(img);
}
}
} catch (e) {
Get.snackbar(
'Error',
'Gagal memilih gambar dari galeri: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
void checkFormChanged() {
final current = {
'nama': nameController.text,
'deskripsi': descriptionController.text,
'status': selectedStatus.value,
'images': List.from(selectedImages),
'items': List.from(packageItems),
'perJam': timeOptions['Per Jam']?.value ?? false,
'perHari': timeOptions['Per Hari']?.value ?? false,
'hargaJam': pricePerHourController.text,
'maxJam': maxHourController.text,
'hargaHari': pricePerDayController.text,
'maxHari': maxDayController.text,
};
isFormChanged.value = current.toString() != initialFormData.toString();
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/petugas_aset_controller.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../../theme/app_colors_petugas.dart';
import '../widgets/petugas_bumdes_bottom_navbar.dart';
import '../widgets/petugas_side_navbar.dart';
@ -23,26 +24,12 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
void initState() {
super.initState();
controller = Get.find<PetugasAsetController>();
_tabController = TabController(length: 2, vsync: this);
// Listen to tab changes and update controller
_tabController.addListener(() {
if (!_tabController.indexIsChanging) {
controller.changeTab(_tabController.index);
}
});
// Listen to controller tab changes and update TabController
ever(controller.selectedTabIndex, (index) {
if (_tabController.index != index) {
_tabController.animateTo(index);
}
});
// Initialize with default tab (sewa)
controller.changeTab(0);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@ -82,7 +69,7 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
body: Column(
children: [
_buildSearchBar(),
_buildTabBar(),
const SizedBox(height: 16),
Expanded(child: _buildAssetList()),
],
),
@ -93,7 +80,13 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_ASET),
onPressed: () {
// Navigate to PetugasTambahAsetView in add mode
Get.toNamed(
Routes.PETUGAS_TAMBAH_ASET,
arguments: {'isEditing': false, 'assetData': null},
);
},
backgroundColor: AppColorsPetugas.babyBlueBright,
icon: Icon(Icons.add, color: AppColorsPetugas.blueGrotto),
label: Text(
@ -144,60 +137,19 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
);
}
Widget _buildTabBar() {
return Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 0),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
borderRadius: BorderRadius.circular(12),
),
child: TabBar(
controller: _tabController,
labelColor: Colors.white,
unselectedLabelColor: AppColorsPetugas.textSecondary,
indicatorSize: TabBarIndicatorSize.tab,
indicator: BoxDecoration(
color: AppColorsPetugas.blueGrotto,
borderRadius: BorderRadius.circular(12),
),
dividerColor: Colors.transparent,
tabs: const [
Tab(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.shopping_cart, size: 18),
SizedBox(width: 8),
Text('Sewa', style: TextStyle(fontWeight: FontWeight.w600)),
],
),
),
),
Tab(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.subscriptions, size: 18),
SizedBox(width: 8),
Text(
'Langganan',
style: TextStyle(fontWeight: FontWeight.w600),
),
],
),
),
),
],
),
);
}
// Tab bar has been removed as per requirements
Widget _buildAssetList() {
return Obx(() {
debugPrint('_buildAssetList: isLoading=${controller.isLoading.value}');
debugPrint(
'_buildAssetList: filteredAsetList length=${controller.filteredAsetList.length}',
);
if (controller.filteredAsetList.isNotEmpty) {
debugPrint(
'_buildAssetList: First item name=${controller.filteredAsetList[0]['nama']}',
);
}
if (controller.isLoading.value) {
return Center(
child: CircularProgressIndicator(
@ -255,10 +207,15 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
color: AppColorsPetugas.blueGrotto,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: controller.filteredAsetList.length,
itemCount: controller.filteredAsetList.length + 1,
itemBuilder: (context, index) {
final aset = controller.filteredAsetList[index];
return _buildAssetCard(context, aset);
if (index < controller.filteredAsetList.length) {
final aset = controller.filteredAsetList[index];
return _buildAssetCard(context, aset);
} else {
// Blank space at the end
return const SizedBox(height: 80);
}
},
),
);
@ -266,7 +223,31 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
}
Widget _buildAssetCard(BuildContext context, Map<String, dynamic> aset) {
final isAvailable = aset['tersedia'] == true;
debugPrint('\n--- Building Asset Card ---');
debugPrint('Asset data: $aset');
// Extract and validate all asset properties with proper null safety
final status =
aset['status']?.toString().toLowerCase() ?? 'tidak_diketahui';
final isAvailable = status == 'tersedia';
final imageUrl = aset['imageUrl']?.toString() ?? '';
final harga =
aset['harga'] is int
? aset['harga'] as int
: (int.tryParse(aset['harga']?.toString() ?? '0') ?? 0);
final satuanWaktu =
aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari';
final nama = aset['nama']?.toString().trim() ?? 'Nama tidak tersedia';
final kategori = aset['kategori']?.toString().trim() ?? 'Umum';
final orderId = aset['order_id']?.toString() ?? '';
// Debug prints for development
debugPrint('Image URL: $imageUrl');
debugPrint('Harga: $harga');
debugPrint('Satuan Waktu: $satuanWaktu');
debugPrint('Nama: $nama');
debugPrint('Kategori: $kategori');
debugPrint('Status: $status (Available: $isAvailable)');
return Container(
margin: const EdgeInsets.only(bottom: 12),
@ -290,21 +271,46 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
child: Row(
children: [
// Asset image
Container(
SizedBox(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
),
child: Center(
child: Icon(
_getAssetIcon(aset['kategori']),
color: AppColorsPetugas.navyBlue,
size: 32,
child: CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
placeholder:
(context, url) => Container(
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
_getAssetIcon(
kategori,
), // Show category icon as placeholder
color: AppColorsPetugas.navyBlue.withOpacity(
0.5,
),
size: 32,
),
),
),
errorWidget:
(context, url, error) => Container(
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
Icons
.broken_image, // Or your preferred error icon
color: AppColorsPetugas.navyBlue.withOpacity(
0.5,
),
size: 32,
),
),
),
),
),
),
@ -323,8 +329,8 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
aset['nama'],
style: TextStyle(
nama,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: AppColorsPetugas.navyBlue,
@ -333,12 +339,63 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'${controller.formatPrice(aset['harga'])} ${aset['satuan']}',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.textSecondary,
),
// Harga dan satuan waktu (multi-line, tampilkan semua dari satuanWaktuSewa)
Builder(
builder: (context) {
final satuanWaktuList =
(aset['satuanWaktuSewa'] is List)
? List<Map<String, dynamic>>.from(
aset['satuanWaktuSewa'],
)
: [];
final validSatuanWaktu =
satuanWaktuList
.where(
(sw) =>
(sw['harga'] ?? 0) > 0 &&
(sw['nama_satuan_waktu'] !=
null &&
(sw['nama_satuan_waktu']
as String)
.isNotEmpty),
)
.toList();
if (validSatuanWaktu.isNotEmpty) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children:
validSatuanWaktu.map((sw) {
final harga = sw['harga'] ?? 0;
final satuan =
sw['nama_satuan_waktu'] ?? '';
return Text(
'${controller.formatPrice(harga)} / $satuan',
style: TextStyle(
fontSize: 12,
color:
AppColorsPetugas
.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}).toList(),
);
} else {
// fallback: harga tunggal
return Text(
'${controller.formatPrice(aset['harga'] ?? 0)} / ${aset['satuan_waktu']?.toString().capitalizeFirst ?? 'Hari'}',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
);
}
},
),
],
),
@ -383,11 +440,36 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
children: [
// Edit icon
GestureDetector(
onTap:
() => _showAddEditAssetDialog(
context,
aset: aset,
),
onTap: () {
// Navigate to PetugasTambahAsetView in edit mode with only the asset ID
final assetId =
aset['id']?.toString() ??
''; // Changed from 'id_aset' to 'id'
debugPrint(
'[DEBUG] Navigating to edit asset with ID: $assetId',
);
debugPrint(
'[DEBUG] Full asset data: $aset',
); // Log full asset data for debugging
if (assetId.isEmpty) {
debugPrint('[ERROR] Asset ID is empty!');
Get.snackbar(
'Error',
'ID Aset tidak valid',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
Get.toNamed(
Routes.PETUGAS_TAMBAH_ASET,
arguments: {
'isEditing': true,
'assetId': assetId,
},
);
},
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(

View File

@ -5,6 +5,7 @@ import '../controllers/petugas_bumdes_dashboard_controller.dart';
import '../widgets/petugas_bumdes_bottom_navbar.dart';
import '../widgets/petugas_side_navbar.dart';
import '../../../theme/app_colors_petugas.dart';
import '../../../utils/format_utils.dart';
class PetugasBumdesDashboardView
extends GetView<PetugasBumdesDashboardController> {
@ -23,12 +24,7 @@ class PetugasBumdesDashboardView
backgroundColor: AppColorsPetugas.navyBlue,
foregroundColor: Colors.white,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => _showLogoutConfirmation(context),
),
],
// actions: [],
),
drawer: PetugasSideNavbar(controller: controller),
drawerEdgeDragWidth: 60,
@ -118,8 +114,6 @@ class PetugasBumdesDashboardView
),
_buildRevenueStatistics(),
const SizedBox(height: 16),
_buildRevenueSources(),
const SizedBox(height: 16),
_buildRevenueTrend(),
// Add some padding at the bottom for better scrolling
@ -156,25 +150,51 @@ class PetugasBumdesDashboardView
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 3),
Obx(() {
final avatar = controller.avatarUrl.value;
if (avatar.isNotEmpty) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
avatar,
width: 48,
height: 48,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Container(
width: 48,
height: 48,
color: Colors.white.withOpacity(0.2),
child: const Icon(
Icons.person,
color: Colors.white,
size: 30,
),
),
),
],
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 30,
),
),
);
} else {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
),
child: const Icon(
Icons.person,
color: Colors.white,
size: 30,
),
);
}
}),
const SizedBox(width: 16),
Expanded(
child: Column(
@ -208,15 +228,17 @@ class PetugasBumdesDashboardView
),
),
const SizedBox(height: 4),
Obx(
() => Text(
controller.userEmail.value,
Obx(() {
final name = controller.userName.value;
final email = controller.userEmail.value;
return Text(
name.isNotEmpty ? name : email,
style: const TextStyle(
fontSize: 14,
color: Colors.white70,
),
),
),
);
}),
],
),
),
@ -642,19 +664,24 @@ class PetugasBumdesDashboardView
),
),
const SizedBox(height: 10),
Obx(
() => Text(
controller.totalPendapatanBulanIni.value,
Obx(() {
final stats = controller.pembayaranStats;
final total = stats['totalThisMonth'] ?? 0.0;
return Text(
formatRupiah(total),
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.success,
),
),
),
);
}),
const SizedBox(height: 6),
Obx(
() => Row(
Obx(() {
final stats = controller.pembayaranStats;
final percent = stats['percentComparedLast'] ?? 0.0;
final isPositive = percent >= 0;
return Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
@ -663,7 +690,7 @@ class PetugasBumdesDashboardView
),
decoration: BoxDecoration(
color:
controller.isKenaikanPositif.value
isPositive
? AppColorsPetugas.success.withOpacity(
0.1,
)
@ -676,23 +703,23 @@ class PetugasBumdesDashboardView
mainAxisSize: MainAxisSize.min,
children: [
Icon(
controller.isKenaikanPositif.value
isPositive
? Icons.arrow_upward
: Icons.arrow_downward,
size: 14,
color:
controller.isKenaikanPositif.value
isPositive
? AppColorsPetugas.success
: AppColorsPetugas.error,
),
const SizedBox(width: 4),
Text(
controller.persentaseKenaikan.value,
'${percent.toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color:
controller.isKenaikanPositif.value
isPositive
? AppColorsPetugas.success
: AppColorsPetugas.error,
),
@ -709,8 +736,8 @@ class PetugasBumdesDashboardView
),
),
],
),
),
);
}),
],
),
),
@ -747,12 +774,29 @@ class PetugasBumdesDashboardView
return Row(
children: [
Expanded(
child: _buildRevenueQuickInfo(
'Pendapatan Sewa',
controller.pendapatanSewa.value,
AppColorsPetugas.navyBlue,
Icons.shopping_cart_outlined,
),
child: Obx(() {
final stats = controller.pembayaranStats;
final totalTunai = stats['totalTunai'] ?? 0.0;
return _buildRevenueQuickInfo(
'Tunai',
formatRupiah(totalTunai),
AppColorsPetugas.navyBlue,
Icons.payments,
);
}),
),
const SizedBox(width: 12),
Expanded(
child: Obx(() {
final stats = controller.pembayaranStats;
final totalTransfer = stats['totalTransfer'] ?? 0.0;
return _buildRevenueQuickInfo(
'Transfer',
formatRupiah(totalTransfer),
AppColorsPetugas.blueGrotto,
Icons.account_balance,
);
}),
),
],
);
@ -811,81 +855,6 @@ class PetugasBumdesDashboardView
);
}
Widget _buildRevenueSources() {
return Card(
elevation: 2,
shadowColor: AppColorsPetugas.shadowColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sumber Pendapatan',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 20),
Row(
children: [
// Revenue Donut Chart
Expanded(
flex: 2,
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColorsPetugas.navyBlue.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(
'Sewa Aset',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 8),
Obx(
() => Text(
controller.pendapatanSewa.value,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
),
const SizedBox(height: 8),
Text(
'100% dari total pendapatan',
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade700,
),
),
],
),
),
],
),
),
],
),
],
),
),
);
}
Widget _buildRevenueTrend() {
final months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun'];
@ -912,6 +881,9 @@ class PetugasBumdesDashboardView
child: Obx(() {
// Get the trend data from controller
final List<double> trendData = controller.trendPendapatan;
if (trendData.isEmpty) {
return Center(child: Text('Tidak ada data'));
}
final double maxValue = trendData.reduce(
(curr, next) => curr > next ? curr : next,
);
@ -925,28 +897,28 @@ class PetugasBumdesDashboardView
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${maxValue.toStringAsFixed(1)}M',
formatRupiah(maxValue),
style: TextStyle(
fontSize: 10,
color: AppColorsPetugas.textSecondary,
),
),
Text(
'${(maxValue * 0.75).toStringAsFixed(1)}M',
formatRupiah(maxValue * 0.75),
style: TextStyle(
fontSize: 10,
color: AppColorsPetugas.textSecondary,
),
),
Text(
'${(maxValue * 0.5).toStringAsFixed(1)}M',
formatRupiah(maxValue * 0.5),
style: TextStyle(
fontSize: 10,
color: AppColorsPetugas.textSecondary,
),
),
Text(
'${(maxValue * 0.25).toStringAsFixed(1)}M',
formatRupiah(maxValue * 0.25),
style: TextStyle(
fontSize: 10,
color: AppColorsPetugas.textSecondary,

View File

@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/petugas_paket_controller.dart';
import '../../../theme/app_colors_petugas.dart';
import 'package:bumrent_app/app/modules/petugas_bumdes/controllers/petugas_paket_controller.dart';
import 'package:bumrent_app/app/routes/app_pages.dart';
import 'package:bumrent_app/app/data/models/paket_model.dart';
import '../widgets/petugas_bumdes_bottom_navbar.dart';
import '../widgets/petugas_side_navbar.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart';
import '../../../routes/app_routes.dart';
import '../../../theme/app_colors_petugas.dart';
class PetugasPaketView extends GetView<PetugasPaketController> {
const PetugasPaketView({Key? key}) : super(key: key);
@ -53,7 +55,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
),
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
onPressed:
() => Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET,
arguments: {'isEditing': false},
),
label: Text(
'Tambah Paket',
style: TextStyle(
@ -115,7 +121,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
);
}
if (controller.filteredPaketList.isEmpty) {
if (controller.filteredPackages.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -136,7 +142,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: () => Get.toNamed(Routes.PETUGAS_TAMBAH_PAKET),
onPressed:
() => Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET,
arguments: {'isEditing': false},
),
icon: const Icon(Icons.add),
label: const Text('Tambah Paket'),
style: ElevatedButton.styleFrom(
@ -161,18 +171,192 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
color: AppColorsPetugas.blueGrotto,
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: controller.filteredPaketList.length,
itemCount: controller.filteredPackages.length + 1,
itemBuilder: (context, index) {
final paket = controller.filteredPaketList[index];
return _buildPaketCard(context, paket);
if (index < controller.filteredPackages.length) {
final paket = controller.filteredPackages[index];
return _buildPaketCard(context, paket);
} else {
// Blank space at the end
return const SizedBox(height: 80);
}
},
),
);
});
}
Widget _buildPaketCard(BuildContext context, Map<String, dynamic> paket) {
final isAvailable = paket['tersedia'] == true;
// Format price helper method
String _formatPrice(dynamic price) {
if (price == null) return '0';
// If price is a string that can be parsed to a number
if (price is String) {
final number = double.tryParse(price) ?? 0;
return number
.toStringAsFixed(0)
.replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
);
}
// If price is already a number
if (price is num) {
return price
.toStringAsFixed(0)
.replaceAllMapped(
RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'),
(Match m) => '${m[1]}.',
);
}
return '0';
}
// Helper method to get time unit name based on ID
String _getTimeUnitName(dynamic unitId) {
if (unitId == null) return 'unit';
// Convert to string in case it's not already
final unitIdStr = unitId.toString().toLowerCase();
// Map of known time unit IDs to their display names
final timeUnitMap = {
'6eaa32d9-855d-4214-b5b5-5c73d3edd9c5': 'jam',
'582b7e66-6869-4495-9856-cef4a46683b0': 'hari',
// Add more mappings as needed
};
// If the unitId is a known ID, return the corresponding name
if (timeUnitMap.containsKey(unitIdStr)) {
return timeUnitMap[unitIdStr]!;
}
// Check if the unit is already a name (like 'jam' or 'hari')
final knownUnits = ['jam', 'hari', 'minggu', 'bulan'];
if (knownUnits.contains(unitIdStr)) {
return unitIdStr;
}
// If the unit is a Map, try to extract the name from common fields
if (unitId is Map) {
return unitId['nama']?.toString().toLowerCase() ??
unitId['name']?.toString().toLowerCase() ??
unitId['satuan_waktu']?.toString().toLowerCase() ??
'unit';
}
// Default fallback
return 'unit';
}
// Helper method to log time unit details
void _logTimeUnitDetails(
String packageName,
List<Map<String, dynamic>> timeUnits,
) {
debugPrint('\n📦 [DEBUG] Package: $packageName');
debugPrint('🔄 Found ${timeUnits.length} time units:');
for (var i = 0; i < timeUnits.length; i++) {
final unit = timeUnits[i];
debugPrint('\n ⏱️ Time Unit #${i + 1}:');
// Log all available keys and values
debugPrint(' ├─ All fields: $unit');
// Log specific fields we're interested in
unit.forEach((key, value) {
debugPrint(' ├─ $key: $value (${value.runtimeType})');
});
// Special handling for satuan_waktu if it's a map
if (unit['satuan_waktu'] is Map) {
final satuanWaktu = unit['satuan_waktu'] as Map;
debugPrint(' └─ satuan_waktu details:');
satuanWaktu.forEach((k, v) {
debugPrint(' ├─ $k: $v (${v.runtimeType})');
});
}
}
debugPrint('\n');
}
Widget _buildPaketCard(BuildContext context, dynamic paket) {
// Handle both Map and PaketModel for backward compatibility
final isPaketModel = paket is PaketModel;
debugPrint('\n🔍 [_buildPaketCard] Paket type: ${paket.runtimeType}');
debugPrint('📋 Paket data: $paket');
// Extract status based on type
final String status =
isPaketModel
? (paket.status?.toString().capitalizeFirst ?? 'Tidak Diketahui')
: (paket['status']?.toString().capitalizeFirst ??
'Tidak Diketahui');
debugPrint('🏷️ Extracted status: $status (isPaketModel: $isPaketModel)');
// Extract availability based on type
final bool isAvailable =
isPaketModel
? (paket.kuantitas > 0)
: ((paket['kuantitas'] as int?) ?? 0) > 0;
final String nama =
isPaketModel
? paket.nama
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
// Debug package info
debugPrint('\n📦 [PACKAGE] ${paket.runtimeType} - $nama');
debugPrint('├─ isPaketModel: $isPaketModel');
debugPrint('├─ Available: $isAvailable');
// Get the first rental time unit price if available, otherwise use the base price
final dynamic harga;
if (isPaketModel) {
if (paket.satuanWaktuSewa.isNotEmpty) {
_logTimeUnitDetails(nama, paket.satuanWaktuSewa);
// Get the first time unit with its price
final firstUnit = paket.satuanWaktuSewa.first;
final firstUnitPrice = firstUnit['harga'];
debugPrint('💰 First time unit price: $firstUnitPrice');
debugPrint('⏱️ First time unit ID: ${firstUnit['satuan_waktu_id']}');
debugPrint('📝 First time unit details: $firstUnit');
// Always use the first time unit's price if available
harga = firstUnitPrice ?? 0;
} else {
debugPrint('⚠️ No time units found for package: $nama');
debugPrint(' Using base price: ${paket.harga}');
harga = paket.harga;
}
} else {
// For non-PaketModel (Map) data
if (isPaketModel && paket.satuanWaktuSewa.isNotEmpty) {
final firstUnit = paket.satuanWaktuSewa.first;
final firstUnitPrice = firstUnit['harga'];
debugPrint('💰 [MAP] First time unit price: $firstUnitPrice');
harga = firstUnitPrice ?? 0;
} else {
debugPrint('⚠️ [MAP] No time units found for package: $nama');
debugPrint(' [MAP] Using base price: ${paket['harga']}');
harga = paket['harga'] ?? 0;
}
}
debugPrint('💵 Final price being used: $harga\n');
// Get the main photo URL
final String? foto =
isPaketModel
? (paket.images?.isNotEmpty == true
? paket.images!.first
: paket.foto_paket)
: (paket['foto_paket']?.toString() ??
(paket['foto'] is String ? paket['foto'] : null));
return Container(
margin: const EdgeInsets.only(bottom: 12),
@ -196,22 +380,83 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
child: Row(
children: [
// Paket image or icon
Container(
SizedBox(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
child: ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
bottomLeft: Radius.circular(12),
),
),
child: Center(
child: Icon(
_getPaketIcon(paket['kategori']),
color: AppColorsPetugas.navyBlue,
size: 32,
),
child:
foto != null && foto.isNotEmpty
? Image.network(
foto,
width: 80,
height: 80,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Container(
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
_getPaketIcon(
_getTimeUnitName(
isPaketModel
? (paket
.satuanWaktuSewa
.isNotEmpty
? paket
.satuanWaktuSewa
.first['satuan_waktu_id'] ??
'hari'
: 'hari')
: (paket['satuanWaktuSewa'] !=
null &&
paket['satuanWaktuSewa']
.isNotEmpty
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
?.toString() ??
'hari'
: 'hari'),
),
),
color: AppColorsPetugas.navyBlue
.withOpacity(0.5),
size: 32,
),
),
),
)
: Container(
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
_getPaketIcon(
_getTimeUnitName(
isPaketModel
? (paket.satuanWaktuSewa.isNotEmpty
? paket
.satuanWaktuSewa
.first['satuan_waktu_id'] ??
'hari'
: 'hari')
: (paket['satuanWaktuSewa'] != null &&
paket['satuanWaktuSewa']
.isNotEmpty
? paket['satuanWaktuSewa'][0]['satuan_waktu_id']
?.toString() ??
'hari'
: 'hari'),
),
),
color: AppColorsPetugas.navyBlue.withOpacity(
0.5,
),
size: 32,
),
),
),
),
),
@ -228,9 +473,10 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Package name
Text(
paket['nama'],
style: TextStyle(
nama,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: AppColorsPetugas.navyBlue,
@ -239,13 +485,119 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'Rp ${_formatPrice(paket['harga'])}',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.textSecondary,
),
// Prices with time units
Builder(
builder: (context) {
final List<Map<String, dynamic>> timeUnits =
[];
// Get all time units
if (isPaketModel &&
paket.satuanWaktuSewa.isNotEmpty) {
timeUnits.addAll(paket.satuanWaktuSewa);
} else if (!isPaketModel &&
paket['satuanWaktuSewa'] != null &&
paket['satuanWaktuSewa'].isNotEmpty) {
timeUnits.addAll(
List<Map<String, dynamic>>.from(
paket['satuanWaktuSewa'],
),
);
}
// If no time units, show nothing
if (timeUnits.isEmpty)
return const SizedBox.shrink();
// Filter out time units with price 0 or null
final validTimeUnits =
timeUnits.where((unit) {
final price =
unit['harga'] is int
? unit['harga']
: int.tryParse(
unit['harga']
?.toString() ??
'0',
) ??
0;
return price > 0;
}).toList();
if (validTimeUnits.isEmpty)
return const SizedBox.shrink();
return Column(
children:
validTimeUnits
.asMap()
.entries
.map((entry) {
final index = entry.key;
final unit = entry.value;
final unitPrice =
unit['harga'] is int
? unit['harga']
: int.tryParse(
unit['harga']
?.toString() ??
'0',
) ??
0;
final unitName = _getTimeUnitName(
unit['satuan_waktu_id'],
);
final isFirst = index == 0;
if (unitPrice <= 0)
return const SizedBox.shrink();
return Row(
children: [
Flexible(
child: Text(
'Rp ${_formatPrice(unitPrice)}/$unitName',
style: TextStyle(
fontSize: 12,
color:
AppColorsPetugas
.textSecondary,
),
maxLines: 2,
overflow:
TextOverflow.ellipsis,
softWrap: true,
),
),
],
);
})
.where(
(widget) => widget is! SizedBox,
)
.toList(),
);
},
),
if (!isPaketModel &&
paket['harga'] != null &&
(paket['harga'] is int
? paket['harga']
: int.tryParse(
paket['harga']?.toString() ??
'0',
) ??
0) >
0) ...[
const SizedBox(height: 4),
Text(
'Rp ${_formatPrice(paket['harga'])}',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.textSecondary,
),
),
],
],
),
),
@ -258,25 +610,31 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
),
decoration: BoxDecoration(
color:
isAvailable
status.toLowerCase() == 'tersedia'
? AppColorsPetugas.successLight
: status.toLowerCase() == 'pemeliharaan'
? AppColorsPetugas.warningLight
: AppColorsPetugas.errorLight,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
isAvailable
status.toLowerCase() == 'tersedia'
? AppColorsPetugas.success
: status.toLowerCase() == 'pemeliharaan'
? AppColorsPetugas.warning
: AppColorsPetugas.error,
width: 1,
),
),
child: Text(
isAvailable ? 'Aktif' : 'Nonaktif',
status,
style: TextStyle(
fontSize: 10,
color:
isAvailable
status.toLowerCase() == 'tersedia'
? AppColorsPetugas.success
: status.toLowerCase() == 'pemeliharaan'
? AppColorsPetugas.warning
: AppColorsPetugas.error,
fontWeight: FontWeight.w500,
),
@ -290,9 +648,12 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
// Edit icon
GestureDetector(
onTap:
() => _showAddEditPaketDialog(
context,
paket: paket,
() => Get.toNamed(
Routes.PETUGAS_TAMBAH_PAKET,
arguments: {
'isEditing': true,
'paket': paket,
},
),
child: Container(
padding: const EdgeInsets.all(5),
@ -350,33 +711,42 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
);
}
String _formatPrice(dynamic price) {
if (price == null) return '0';
// Convert the price to string and handle formatting
String priceStr = price.toString();
// Add thousand separators
final RegExp reg = RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))');
String formatted = priceStr.replaceAllMapped(reg, (Match m) => '${m[1]}.');
return formatted;
// Add this helper method to get color based on status
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'aktif':
return AppColorsPetugas.success;
case 'tidak aktif':
case 'nonaktif':
return AppColorsPetugas.error;
case 'dalam perbaikan':
case 'maintenance':
return AppColorsPetugas.warning;
case 'tersedia':
return AppColorsPetugas.success;
case 'pemeliharaan':
return AppColorsPetugas.warning;
default:
return Colors.grey;
}
}
IconData _getPaketIcon(String? category) {
if (category == null) return Icons.category;
IconData _getPaketIcon(String? timeUnit) {
if (timeUnit == null) return Icons.access_time;
switch (category.toLowerCase()) {
case 'bulanan':
return Icons.calendar_month;
case 'tahunan':
switch (timeUnit.toLowerCase()) {
case 'jam':
return Icons.access_time;
case 'hari':
return Icons.calendar_today;
case 'premium':
return Icons.star;
case 'bisnis':
return Icons.business;
case 'minggu':
return Icons.date_range;
case 'bulan':
return Icons.calendar_month;
case 'tahun':
return Icons.calendar_view_month;
default:
return Icons.category;
return Icons.access_time;
}
}
@ -426,7 +796,27 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
);
}
void _showPaketDetails(BuildContext context, Map<String, dynamic> paket) {
void _showPaketDetails(BuildContext context, dynamic paket) {
// Handle both Map and PaketModel for backward compatibility
final isPaketModel = paket is PaketModel;
final String nama =
isPaketModel
? paket.nama
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
final String? deskripsi =
isPaketModel ? paket.deskripsi : paket['deskripsi']?.toString();
final bool isAvailable =
isPaketModel
? (paket.kuantitas > 0)
: ((paket['kuantitas'] as int?) ?? 0) > 0;
final dynamic harga =
isPaketModel
? (paket.satuanWaktuSewa.isNotEmpty
? paket.satuanWaktuSewa.first['harga']
: paket.harga)
: (paket['harga'] ?? 0);
// Items are not part of the PaketModel, so we'll use an empty list
final List<Map<String, dynamic>> items = [];
showModalBottomSheet(
context: context,
isScrollControlled: true,
@ -448,7 +838,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
children: [
Expanded(
child: Text(
paket['nama'],
nama,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
@ -473,16 +863,15 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildDetailItem('Kategori', paket['kategori']),
_buildDetailItem(
'Harga',
controller.formatPrice(paket['harga']),
'Rp ${_formatPrice(harga)}',
),
_buildDetailItem(
'Status',
paket['tersedia'] ? 'Tersedia' : 'Tidak Tersedia',
isAvailable ? 'Tersedia' : 'Tidak Tersedia',
),
_buildDetailItem('Deskripsi', paket['deskripsi']),
_buildDetailItem('Deskripsi', deskripsi ?? '-'),
],
),
),
@ -502,11 +891,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
child: ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: paket['items'].length,
itemCount: items.length,
separatorBuilder:
(context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
final item = paket['items'][index];
final item = items[index];
return ListTile(
leading: CircleAvatar(
backgroundColor: AppColorsPetugas.babyBlue,
@ -601,10 +990,11 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
);
}
void _showAddEditPaketDialog(
BuildContext context, {
Map<String, dynamic>? paket,
}) {
void _showAddEditPaketDialog(BuildContext context, {dynamic paket}) {
// Handle both Map and PaketModel for backward compatibility
final isPaketModel = paket is PaketModel;
final String? id = isPaketModel ? paket.id : paket?['id'];
final String title = id == null ? 'Tambah Paket' : 'Edit Paket';
final isEditing = paket != null;
// This would be implemented with proper form validation in a real app
@ -613,7 +1003,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
builder: (context) {
return AlertDialog(
title: Text(
isEditing ? 'Edit Paket' : 'Tambah Paket Baru',
title,
style: TextStyle(color: AppColorsPetugas.navyBlue),
),
content: const Text(
@ -652,10 +1042,13 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
);
}
void _showDeleteConfirmation(
BuildContext context,
Map<String, dynamic> paket,
) {
void _showDeleteConfirmation(BuildContext context, dynamic paket) {
// Handle both Map and PaketModel for backward compatibility
final isPaketModel = paket is PaketModel;
final String id = isPaketModel ? paket.id : (paket['id']?.toString() ?? '');
final String nama =
isPaketModel ? paket.nama : (paket['nama']?.toString() ?? 'Paket');
showDialog(
context: context,
builder: (context) {
@ -664,9 +1057,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
'Konfirmasi Hapus',
style: TextStyle(color: AppColorsPetugas.navyBlue),
),
content: Text(
'Apakah Anda yakin ingin menghapus paket "${paket['nama']}"?',
),
content: Text('Apakah Anda yakin ingin menghapus paket "$nama"?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
@ -678,7 +1069,7 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
ElevatedButton(
onPressed: () {
Navigator.pop(context);
controller.deletePaket(paket['id']);
controller.deletePaket(id);
Get.snackbar(
'Paket Dihapus',
'Paket berhasil dihapus dari sistem',

View File

@ -6,6 +6,7 @@ import '../widgets/petugas_bumdes_bottom_navbar.dart';
import '../widgets/petugas_side_navbar.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart';
import 'petugas_detail_sewa_view.dart';
import '../../../data/models/rental_booking_model.dart';
class PetugasSewaView extends StatefulWidget {
const PetugasSewaView({Key? key}) : super(key: key);
@ -160,6 +161,10 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
}
Widget _buildSearchSection() {
// Tambahkan controller untuk TextField agar bisa dikosongkan
final TextEditingController searchController = TextEditingController(
text: controller.searchQuery.value,
);
return Container(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
decoration: BoxDecoration(
@ -173,9 +178,9 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
],
),
child: TextField(
controller: searchController,
onChanged: (value) {
controller.setSearchQuery(value);
controller.setOrderIdQuery(value);
},
decoration: InputDecoration(
hintText: 'Cari nama warga atau ID pesanan...',
@ -204,10 +209,21 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
),
contentPadding: EdgeInsets.zero,
isDense: true,
suffixIcon: Icon(
Icons.tune_rounded,
color: AppColorsPetugas.textSecondary,
size: 20,
suffixIcon: Obx(
() =>
controller.searchQuery.value.isNotEmpty
? IconButton(
icon: Icon(
Icons.close,
color: AppColorsPetugas.textSecondary,
size: 20,
),
onPressed: () {
searchController.clear();
controller.setSearchQuery('');
},
)
: SizedBox.shrink(),
),
),
),
@ -241,17 +257,44 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
final filteredList =
status == 'Semua'
? controller.filteredSewaList
: status == 'Menunggu Pembayaran'
? controller.sewaList
.where(
(sewa) =>
sewa.status.toUpperCase() == 'MENUNGGU PEMBAYARAN' ||
sewa.status.toUpperCase() == 'PEMBAYARAN DENDA',
)
.toList()
: status == 'Periksa Pembayaran'
? controller.sewaList
.where(
(sewa) =>
sewa['status'] == 'Periksa Pembayaran' ||
sewa['status'] == 'Pembayaran Denda' ||
sewa['status'] == 'Periksa Denda',
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN' ||
sewa.status.toUpperCase() == 'PERIKSA PEMBAYARAN DENDA',
)
.toList()
: status == 'Diterima'
? controller.sewaList
.where((sewa) => sewa.status.toUpperCase() == 'DITERIMA')
.toList()
: status == 'Aktif'
? controller.sewaList
.where((sewa) => sewa.status.toUpperCase() == 'AKTIF')
.toList()
: status == 'Dikembalikan'
? controller.sewaList
.where((sewa) => sewa.status.toUpperCase() == 'DIKEMBALIKAN')
.toList()
: status == 'Selesai'
? controller.sewaList
.where((sewa) => sewa.status.toUpperCase() == 'SELESAI')
.toList()
: status == 'Dibatalkan'
? controller.sewaList
.where((sewa) => sewa.status.toUpperCase() == 'DIBATALKAN')
.toList()
: controller.sewaList
.where((sewa) => sewa['status'] == status)
.where((sewa) => sewa.status == status)
.toList();
if (filteredList.isEmpty) {
@ -313,40 +356,25 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
});
}
Widget _buildSewaCard(BuildContext context, Map<String, dynamic> sewa) {
final statusColor = controller.getStatusColor(sewa['status']);
final status = sewa['status'];
Widget _buildSewaCard(BuildContext context, SewaModel sewa) {
final statusColor = controller.getStatusColor(sewa.status);
final status = sewa.status;
// Get appropriate icon for status
IconData statusIcon;
switch (status) {
case 'Menunggu Pembayaran':
statusIcon = Icons.payments_outlined;
break;
case 'Periksa Pembayaran':
statusIcon = Icons.fact_check_outlined;
break;
case 'Diterima':
statusIcon = Icons.check_circle_outlined;
break;
case 'Pembayaran Denda':
statusIcon = Icons.money_off_csred_outlined;
break;
case 'Periksa Denda':
statusIcon = Icons.assignment_late_outlined;
break;
case 'Dikembalikan':
statusIcon = Icons.assignment_return_outlined;
break;
case 'Selesai':
statusIcon = Icons.task_alt_outlined;
break;
case 'Dibatalkan':
statusIcon = Icons.cancel_outlined;
break;
default:
statusIcon = Icons.help_outline_rounded;
}
IconData statusIcon = controller.getStatusIcon(status);
// Flag untuk membedakan tipe pesanan
final bool isAset = sewa.tipePesanan == 'tunggal';
final bool isPaket = sewa.tipePesanan == 'paket';
// Pilih nama aset/paket
final String namaAsetAtauPaket =
isAset
? (sewa.asetNama ?? '-')
: (isPaket ? (sewa.paketNama ?? '-') : '-');
// Pilih foto aset/paket jika ingin digunakan
final String? fotoAsetAtauPaket =
isAset ? sewa.asetFoto : (isPaket ? sewa.paketFoto : null);
return Container(
margin: const EdgeInsets.only(bottom: 16),
@ -370,6 +398,35 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Status header inside the card
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.12),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Icon(statusIcon, size: 16, color: statusColor),
const SizedBox(width: 8),
Text(
status,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
child: Row(
@ -378,14 +435,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
CircleAvatar(
radius: 24,
backgroundColor: AppColorsPetugas.babyBlueLight,
child: Text(
sewa['nama_warga'].substring(0, 1).toUpperCase(),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.blueGrotto,
),
),
backgroundImage:
(sewa.wargaAvatar != null &&
sewa.wargaAvatar.isNotEmpty)
? NetworkImage(sewa.wargaAvatar)
: null,
child:
(sewa.wargaAvatar == null || sewa.wargaAvatar.isEmpty)
? Text(
sewa.wargaNama.substring(0, 1).toUpperCase(),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.blueGrotto,
),
)
: null,
),
const SizedBox(width: 16),
@ -395,55 +460,22 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sewa['nama_warga'],
sewa.wargaNama,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.textPrimary,
),
),
const SizedBox(height: 2),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 3,
),
decoration: BoxDecoration(
color: statusColor.withOpacity(0.15),
borderRadius: BorderRadius.circular(30),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
statusIcon,
size: 12,
color: statusColor,
),
const SizedBox(width: 4),
Text(
status,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: statusColor,
),
),
],
),
),
const SizedBox(width: 8),
Text(
'#${sewa['order_id']}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textSecondary,
),
),
],
Text(
'Tanggal Pesan: ' +
(sewa.tanggalPemesanan != null
? '${sewa.tanggalPemesanan.day.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.month.toString().padLeft(2, '0')}-${sewa.tanggalPemesanan.year}'
: '-'),
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.textSecondary,
),
),
],
),
@ -460,7 +492,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
borderRadius: BorderRadius.circular(8),
),
child: Text(
controller.formatPrice(sewa['total_biaya']),
controller.formatPrice(sewa.totalTagihan),
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
@ -481,33 +513,51 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
child: Divider(height: 1, color: Colors.grey.shade200),
),
// Asset details
// Asset/Paket details
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
child: Row(
children: [
// Asset icon
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
// Asset/Paket image or icon
if (fotoAsetAtauPaket != null &&
fotoAsetAtauPaket.isNotEmpty)
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
fotoAsetAtauPaket,
width: 40,
height: 40,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Icon(
Icons.inventory_2_outlined,
size: 28,
color: AppColorsPetugas.blueGrotto,
),
),
)
else
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.inventory_2_outlined,
size: 20,
color: AppColorsPetugas.blueGrotto,
),
),
child: Icon(
Icons.inventory_2_outlined,
size: 20,
color: AppColorsPetugas.blueGrotto,
),
),
const SizedBox(width: 12),
// Asset name and duration
// Asset/Paket name and duration
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
sewa['nama_aset'],
namaAsetAtauPaket,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
@ -524,7 +574,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
),
const SizedBox(width: 4),
Text(
'${sewa['tanggal_mulai']} - ${sewa['tanggal_selesai']}',
'${sewa.waktuMulai.toIso8601String().substring(0, 10)} - ${sewa.waktuSelesai.toIso8601String().substring(0, 10)}',
style: TextStyle(
fontSize: 12,
color: AppColorsPetugas.textSecondary,

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../theme/app_colors_petugas.dart';
@ -9,32 +10,51 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text(
'Tambah Aset',
style: TextStyle(fontWeight: FontWeight.w600),
),
backgroundColor: AppColorsPetugas.navyBlue,
elevation: 0,
centerTitle: true,
),
body: SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildHeaderSection(), _buildFormSection(context)],
return GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
child: Obx(() => Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(
controller.isEditing.value ? 'Edit Aset' : 'Tambah Aset',
style: const TextStyle(fontWeight: FontWeight.w600),
),
backgroundColor: AppColorsPetugas.navyBlue,
elevation: 0,
centerTitle: true,
),
),
bottomNavigationBar: _buildBottomBar(),
body: Stack(
children: [
SafeArea(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderSection(),
_buildFormSection(context),
],
),
),
),
if (controller.isLoading.value)
Container(
color: Colors.black54,
child: const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppColorsPetugas.blueGrotto),
),
),
),
],
),
bottomNavigationBar: _buildBottomBar(),
)),
);
}
Widget _buildHeaderSection() {
return Container(
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.only(top: 10, left: 20, right: 20, bottom: 5), // Reduced padding
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
@ -42,50 +62,8 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
end: Alignment.bottomCenter,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.inventory_2_outlined,
color: Colors.white,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informasi Aset Baru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'Isi data dengan lengkap untuk menambahkan aset',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
],
),
],
child: Container(
height: 12, // Further reduced height
),
);
}
@ -131,69 +109,36 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
_buildImageUploader(),
const SizedBox(height: 24),
// Category Section
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
// Status Section
_buildSectionHeader(icon: Icons.check_circle, title: 'Status'),
const SizedBox(height: 16),
// Category and Status as cards
Row(
children: [
Expanded(
child: _buildCategorySelect(
title: 'Kategori',
options: controller.categoryOptions,
selectedOption: controller.selectedCategory,
onChanged: controller.setCategory,
icon: Icons.inventory_2,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildCategorySelect(
title: 'Status',
options: controller.statusOptions,
selectedOption: controller.selectedStatus,
onChanged: controller.setStatus,
icon: Icons.check_circle,
),
),
],
// Status card
_buildCategorySelect(
title: 'Status',
options: controller.statusOptions,
selectedOption: controller.selectedStatus,
onChanged: controller.setStatus,
icon: Icons.check_circle,
),
const SizedBox(height: 24),
// Quantity Section
_buildSectionHeader(
icon: Icons.format_list_numbered,
title: 'Kuantitas & Pengukuran',
title: 'Kuantitas',
),
const SizedBox(height: 16),
// Quantity fields
Row(
children: [
Expanded(
flex: 2,
child: _buildTextField(
label: 'Kuantitas',
hint: 'Jumlah aset',
controller: controller.quantityController,
isRequired: true,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
prefixIcon: Icons.numbers,
),
),
const SizedBox(width: 12),
Expanded(
flex: 3,
child: _buildTextField(
label: 'Satuan Ukur',
hint: 'contoh: Unit, Buah',
controller: controller.unitOfMeasureController,
prefixIcon: Icons.straighten,
),
),
],
// Quantity field
_buildTextField(
label: 'Kuantitas',
hint: 'Jumlah aset',
controller: controller.quantityController,
isRequired: true,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
prefixIcon: Icons.numbers,
),
const SizedBox(height: 24),
@ -654,6 +599,114 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
);
}
// Show image source options
void _showImageSourceOptions() {
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
Text(
'Pilih Sumber Gambar',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildImageSourceOption(
icon: Icons.camera_alt,
label: 'Kamera',
onTap: () {
Get.back();
controller.pickImageFromCamera();
},
),
_buildImageSourceOption(
icon: Icons.photo_library,
label: 'Galeri',
onTap: () {
Get.back();
controller.pickImageFromGallery();
},
),
],
),
const SizedBox(height: 10),
TextButton(
onPressed: () => Get.back(),
child: const Text('Batal'),
),
],
),
),
isScrollControlled: true,
);
}
Widget _buildImageSourceOption({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
width: 70,
height: 70,
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueBright,
shape: BoxShape.circle,
),
child: Icon(
icon,
size: 30,
color: AppColorsPetugas.blueGrotto,
),
),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontSize: 14,
color: AppColorsPetugas.textPrimary,
),
),
],
),
);
}
Widget _buildImageUploader() {
return Container(
padding: const EdgeInsets.all(16),
@ -696,7 +749,7 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
children: [
// Add button
GestureDetector(
onTap: () => controller.addSampleImage(),
onTap: _showImageSourceOptions,
child: Container(
width: 100,
height: 100,
@ -732,69 +785,107 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
),
// Image previews
...controller.selectedImages.asMap().entries.map((entry) {
final index = entry.key;
return Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
ClipRRect(
...List<Widget>.generate(
controller.selectedImages.length,
(index) => Stack(
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.grey[300]!),
),
child: Obx(
() {
// Check if we have a network URL for this index
if (index < controller.networkImageUrls.length &&
controller.networkImageUrls[index].isNotEmpty) {
// Display network image
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
controller.networkImageUrls[index],
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder: (context, error, stackTrace) {
return const Center(
child: Icon(Icons.error_outline, color: Colors.red),
);
},
),
);
} else {
// Display local file
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: FutureBuilder<File>(
future: File(controller.selectedImages[index].path).exists().then((exists) {
if (exists) {
return File(controller.selectedImages[index].path);
} else {
return File(controller.selectedImages[index].path);
}
}),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data != null) {
return Image.file(
snapshot.data!,
width: double.infinity,
height: double.infinity,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey[200],
child: const Icon(Icons.broken_image, color: Colors.grey),
);
},
);
} else {
return Container(
color: Colors.grey[200],
child: const Center(
child: CircularProgressIndicator(),
),
);
}
},
),
);
}
},
),
),
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => controller.removeImage(index),
child: Container(
width: 100,
height: 100,
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
Icons.image,
color: AppColorsPetugas.blueGrotto,
size: 40,
),
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 1),
),
],
),
child: const Icon(
Icons.close,
size: 16,
color: Colors.red,
),
),
),
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => controller.removeImage(index),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Icon(
Icons.close,
color: AppColorsPetugas.error,
size: 16,
),
),
),
),
],
),
);
}),
),
],
),
).toList(),
],
),
),
@ -850,7 +941,9 @@ class PetugasTambahAsetView extends GetView<PetugasTambahAsetController> {
),
)
: const Icon(Icons.save),
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Aset'),
label: Obx(() => Text(
isSubmitting ? 'Menyimpan...' : (controller.isEditing.value ? 'Simpan Perubahan' : 'Simpan Aset'),
)),
style: ElevatedButton.styleFrom(
backgroundColor: AppColorsPetugas.blueGrotto,
foregroundColor: Colors.white,

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../theme/app_colors_petugas.dart';
import '../controllers/petugas_tambah_paket_controller.dart';
import 'dart:io';
class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
const PetugasTambahPaketView({Key? key}) : super(key: key);
@ -12,9 +13,11 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text(
'Tambah Paket',
style: TextStyle(fontWeight: FontWeight.w600),
title: Obx(
() => Text(
controller.isEditing.value ? 'Edit Paket' : 'Tambah Paket',
style: const TextStyle(fontWeight: FontWeight.w600),
),
),
backgroundColor: AppColorsPetugas.navyBlue,
elevation: 0,
@ -24,7 +27,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [_buildHeaderSection(), _buildFormSection(context)],
children: [_buildFormSection(context)],
),
),
),
@ -32,64 +35,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
);
}
Widget _buildHeaderSection() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColorsPetugas.navyBlue, AppColorsPetugas.blueGrotto],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.category,
color: Colors.white,
size: 28,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Informasi Paket Baru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'Isi data dengan lengkap untuk menambahkan paket',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.8),
),
),
],
),
),
],
),
],
),
);
}
Widget _buildFormSection(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
@ -132,22 +77,22 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
const SizedBox(height: 24),
// Category Section
_buildSectionHeader(icon: Icons.category, title: 'Kategori & Status'),
_buildSectionHeader(icon: Icons.category, title: 'Status'),
const SizedBox(height: 16),
// Category and Status as cards
Row(
children: [
Expanded(
child: _buildCategorySelect(
title: 'Kategori',
options: controller.categoryOptions,
selectedOption: controller.selectedCategory,
onChanged: controller.setCategory,
icon: Icons.category,
),
),
const SizedBox(width: 12),
// Expanded(
// child: _buildCategorySelect(
// title: 'Kategori',
// options: controller.categoryOptions,
// selectedOption: controller.selectedCategory,
// onChanged: controller.setCategory,
// icon: Icons.category,
// ),
// ),
// const SizedBox(width: 12),
Expanded(
child: _buildCategorySelect(
title: 'Status',
@ -161,24 +106,6 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
),
const SizedBox(height: 24),
// Price Section
_buildSectionHeader(
icon: Icons.monetization_on,
title: 'Harga Paket',
),
const SizedBox(height: 16),
_buildTextField(
label: 'Harga Paket',
hint: 'Masukkan harga paket',
controller: controller.priceController,
isRequired: true,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
prefixText: 'Rp ',
prefixIcon: Icons.payments,
),
const SizedBox(height: 24),
// Package Items Section
_buildSectionHeader(
icon: Icons.inventory_2,
@ -186,6 +113,40 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
),
const SizedBox(height: 16),
_buildPackageItems(),
const SizedBox(height: 24),
_buildSectionHeader(
icon: Icons.schedule,
title: 'Opsi Waktu & Harga Sewa',
),
const SizedBox(height: 16),
_buildTimeOptionsCards(),
const SizedBox(height: 16),
Obx(
() => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (controller.timeOptions['Per Jam']!.value)
_buildPriceCard(
title: 'Harga Per Jam',
icon: Icons.timer,
priceController: controller.pricePerHourController,
maxController: controller.maxHourController,
maxLabel: 'Maksimal Jam',
),
if (controller.timeOptions['Per Jam']!.value &&
controller.timeOptions['Per Hari']!.value)
const SizedBox(height: 16),
if (controller.timeOptions['Per Hari']!.value)
_buildPriceCard(
title: 'Harga Per Hari',
icon: Icons.calendar_today,
priceController: controller.pricePerDayController,
maxController: controller.maxDayController,
maxLabel: 'Maksimal Hari',
),
],
),
),
const SizedBox(height: 40),
],
),
@ -310,7 +271,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
const SizedBox(height: 16),
// Asset dropdown
DropdownButtonFormField<int>(
DropdownButtonFormField<String>(
value: controller.selectedAsset.value,
decoration: const InputDecoration(
labelText: 'Pilih Aset',
@ -319,8 +280,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
hint: const Text('Pilih Aset'),
items:
controller.availableAssets.map((asset) {
return DropdownMenuItem<int>(
value: asset['id'] as int,
return DropdownMenuItem<String>(
value: asset['id'].toString(),
child: Text(
'${asset['nama']} (Stok: ${asset['stok']})',
),
@ -422,7 +383,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
const SizedBox(height: 16),
// Asset dropdown
DropdownButtonFormField<int>(
DropdownButtonFormField<String>(
value: controller.selectedAsset.value,
decoration: const InputDecoration(
labelText: 'Pilih Aset',
@ -431,8 +392,8 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
hint: const Text('Pilih Aset'),
items:
controller.availableAssets.map((asset) {
return DropdownMenuItem<int>(
value: asset['id'] as int,
return DropdownMenuItem<String>(
value: asset['id'].toString(),
child: Text(
'${asset['nama']} (Stok: ${asset['stok']})',
),
@ -757,7 +718,7 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
children: [
// Add button
GestureDetector(
onTap: () => controller.addSampleImage(),
onTap: _showImageSourceOptions,
child: Container(
width: 100,
height: 100,
@ -791,69 +752,82 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
),
),
),
// Image previews
...controller.selectedImages.asMap().entries.map((entry) {
final index = entry.key;
return Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueLight,
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
child: Stack(
children: [
ClipRRect(
...List<Widget>.generate(controller.selectedImages.length, (
index,
) {
final img = controller.selectedImages[index];
return Stack(
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
child: Container(
width: 100,
height: 100,
color: AppColorsPetugas.babyBlueLight,
child: Center(
child: Icon(
Icons.image,
color: AppColorsPetugas.blueGrotto,
size: 40,
),
),
),
border: Border.all(color: Colors.grey[300]!),
),
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => controller.removeImage(index),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 3,
offset: const Offset(0, 1),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child:
(img is String && img.startsWith('http'))
? Image.network(
img,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder:
(context, error, stackTrace) =>
const Center(
child: Icon(
Icons.broken_image,
color: Colors.grey,
),
),
)
: (img is String)
? Container(
color: Colors.grey[200],
child: const Icon(
Icons.broken_image,
color: Colors.grey,
),
)
: Image.file(
File(img.path),
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
errorBuilder:
(context, error, stackTrace) =>
const Center(
child: Icon(
Icons.broken_image,
color: Colors.grey,
),
),
),
],
),
child: Icon(
Icons.close,
color: AppColorsPetugas.error,
size: 16,
),
),
),
Positioned(
top: 4,
right: 4,
child: InkWell(
onTap: () => controller.removeImage(index),
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
size: 18,
color: Colors.red,
),
),
),
],
),
),
],
);
}),
],
@ -864,6 +838,104 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
);
}
void _showImageSourceOptions() {
Get.bottomSheet(
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
Text(
'Pilih Sumber Gambar',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColorsPetugas.navyBlue,
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildImageSourceOption(
icon: Icons.camera_alt,
label: 'Kamera',
onTap: () {
Get.back();
controller.pickImageFromCamera();
},
),
_buildImageSourceOption(
icon: Icons.photo_library,
label: 'Galeri',
onTap: () {
Get.back();
controller.pickImageFromGallery();
},
),
],
),
const SizedBox(height: 10),
],
),
),
);
}
Widget _buildImageSourceOption({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColorsPetugas.babyBlueBright,
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: AppColorsPetugas.blueGrotto, size: 28),
),
const SizedBox(height: 8),
Text(
label,
style: TextStyle(
fontSize: 14,
color: AppColorsPetugas.navyBlue,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildBottomBar() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
@ -899,26 +971,37 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
final isSubmitting = controller.isSubmitting.value;
return ElevatedButton.icon(
onPressed:
isValid && !isSubmitting ? controller.savePaket : null,
controller.isFormChanged.value && !isSubmitting
? controller.savePaket
: null,
icon:
isSubmitting
? SizedBox(
height: 20,
width: 20,
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.save),
label: Text(isSubmitting ? 'Menyimpan...' : 'Simpan Paket'),
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: 12),
elevation: 0,
padding: const EdgeInsets.symmetric(vertical: 16),
textStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
borderRadius: BorderRadius.circular(12),
),
disabledBackgroundColor: AppColorsPetugas.textLight,
),
@ -929,4 +1012,226 @@ class PetugasTambahPaketView extends GetView<PetugasTambahPaketController> {
),
);
}
Widget _buildTimeOptionsCards() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children:
controller.timeOptions.entries.map((entry) {
final option = entry.key;
final isSelected = entry.value;
return Obx(
() => Material(
color: Colors.transparent,
child: InkWell(
onTap: () => controller.toggleTimeOption(option),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color:
isSelected.value
? AppColorsPetugas.blueGrotto.withOpacity(
0.1,
)
: Colors.grey.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
option == 'Per Jam'
? Icons.hourglass_bottom
: Icons.calendar_today,
color:
isSelected.value
? AppColorsPetugas.blueGrotto
: Colors.grey,
size: 20,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color:
isSelected.value
? AppColorsPetugas.navyBlue
: Colors.grey.shade700,
),
),
const SizedBox(height: 2),
Text(
option == 'Per Jam'
? 'Sewa paket dengan basis perhitungan per jam'
: 'Sewa paket dengan basis perhitungan per hari',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
Checkbox(
value: isSelected.value,
onChanged:
(_) => controller.toggleTimeOption(option),
activeColor: AppColorsPetugas.blueGrotto,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
),
),
);
}).toList(),
),
);
}
Widget _buildPriceCard({
required String title,
required IconData icon,
required TextEditingController priceController,
required TextEditingController maxController,
required String maxLabel,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColorsPetugas.babyBlue.withOpacity(0.5)),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.03),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 20, color: AppColorsPetugas.blueGrotto),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColorsPetugas.navyBlue,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Harga Sewa',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: priceController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
hintText: 'Masukkan harga',
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
prefixText: 'Rp ',
filled: true,
fillColor: AppColorsPetugas.babyBlueBright,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
maxLabel,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColorsPetugas.textSecondary,
),
),
const SizedBox(height: 8),
TextFormField(
controller: maxController,
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: InputDecoration(
hintText: 'Opsional',
hintStyle: TextStyle(color: AppColorsPetugas.textLight),
filled: true,
fillColor: AppColorsPetugas.babyBlueBright,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
),
),
],
),
),
],
),
],
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../theme/app_colors.dart';
import '../../../theme/app_colors_petugas.dart';
import '../controllers/petugas_bumdes_dashboard_controller.dart';
class PetugasSideNavbar extends StatelessWidget {
@ -11,7 +12,7 @@ class PetugasSideNavbar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Drawer(
backgroundColor: Colors.white,
backgroundColor: AppColorsPetugas.babyBlueLight,
elevation: 0,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
@ -32,24 +33,46 @@ class PetugasSideNavbar extends StatelessWidget {
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.fromLTRB(20, 60, 20, 20),
color: AppColors.primary,
color: AppColorsPetugas.navyBlue,
width: double.infinity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(Icons.person, color: AppColors.primary, size: 36),
),
),
Obx(() {
final avatar = controller.avatarUrl.value;
if (avatar.isNotEmpty) {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
backgroundImage: NetworkImage(avatar),
onBackgroundImageError: (error, stackTrace) {},
),
);
} else {
return Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 2),
),
child: CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(
Icons.person,
color: AppColors.primary,
size: 36,
),
),
);
}
}),
const SizedBox(width: 16),
Expanded(
child: Column(

View File

@ -34,7 +34,7 @@ class SplashView extends GetView<SplashController> {
child: Container(
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/pattern.png'),
image: AssetImage('assets/images/logo.png'), // Using logo.png which exists
repeat: ImageRepeat.repeat,
scale: 4.0,
),

View File

@ -8,17 +8,11 @@ import '../../../data/providers/aset_provider.dart';
class WargaSewaBinding extends Bindings {
@override
void dependencies() {
// Ensure NavigationService is registered and set to Sewa tab
if (Get.isRegistered<NavigationService>()) {
final navService = Get.find<NavigationService>();
navService.setNavIndex(1); // Set to Sewa tab
}
// Ensure AuthProvider is registered
if (!Get.isRegistered<AuthProvider>()) {
Get.put(AuthProvider(), permanent: true);
}
// Ensure AsetProvider is registered
if (!Get.isRegistered<AsetProvider>()) {
Get.put(AsetProvider(), permanent: true);

View File

@ -88,6 +88,19 @@ class SewaAsetController extends GetxController
void onReady() {
super.onReady();
debugPrint('🚀 SewaAsetController: onReady called');
// Set tab index from arguments (if any) after build
Future.delayed(Duration.zero, () {
final args = Get.arguments;
if (args != null && args is Map && args['tab'] != null) {
int initialTab =
args['tab'] is int
? args['tab']
: int.tryParse(args['tab'].toString()) ?? 0;
if (tabController.length > initialTab) {
tabController.index = initialTab;
}
}
});
}
@override

View File

@ -2,11 +2,14 @@ import 'package:get/get.dart';
import '../../../data/providers/auth_provider.dart';
import '../../../routes/app_routes.dart';
import '../../../services/navigation_service.dart';
import '../../../data/providers/aset_provider.dart';
import 'package:intl/intl.dart';
class WargaDashboardController extends GetxController {
// Dependency injection
final AuthProvider _authProvider = Get.find<AuthProvider>();
final NavigationService navigationService = Get.find<NavigationService>();
final AsetProvider _asetProvider = Get.find<AsetProvider>();
// User data
final userName = 'Pengguna Warga'.obs;
@ -28,6 +31,11 @@ class WargaDashboardController extends GetxController {
// Active penalties
final activePenalties = <Map<String, dynamic>>[].obs;
// Summary counts
final diterimaCount = 0.obs;
final tagihanAktifCount = 0.obs;
final dendaAktifCount = 0.obs;
@override
void onInit() {
super.onInit();
@ -36,6 +44,7 @@ class WargaDashboardController extends GetxController {
navigationService.setNavIndex(0);
// Load user data
fetchProfileFromWargaDesa();
_loadUserData();
// Load sample data
@ -46,6 +55,12 @@ class WargaDashboardController extends GetxController {
// Load unpaid rentals
loadUnpaidRentals();
// Debug count sewa_aset by status
_debugCountSewaAset();
// Load sewa aktif
loadActiveRentals();
}
Future<void> _loadUserData() async {
@ -112,7 +127,7 @@ class WargaDashboardController extends GetxController {
}
void refreshData() {
// Refresh data from repository
fetchProfileFromWargaDesa();
_loadSampleData();
loadDummyData();
}
@ -129,12 +144,17 @@ class WargaDashboardController extends GetxController {
// Already on Home tab
break;
case 1:
// Navigate to Sewa page
navigationService.toWargaSewa();
// Navigate to Sewa page, tab Aktif
toWargaSewaTabAktif();
break;
}
}
void toWargaSewaTabAktif() {
// Navigasi ke halaman warga sewa dan tab Aktif (index 3)
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 3});
}
void logout() async {
await _authProvider.signOut();
navigationService.toLogin();
@ -177,4 +197,137 @@ class WargaDashboardController extends GetxController {
print('Error loading unpaid rentals: $e');
}
}
Future<void> _debugCountSewaAset() async {
diterimaCount.value = await _asetProvider.countSewaAsetByStatus([
'DITERIMA',
]);
tagihanAktifCount.value = await _asetProvider.countSewaAsetByStatus([
'MENUNGGU PEMBAYARAN',
'PERIKSA PEMBAYARAN',
]);
dendaAktifCount.value = await _asetProvider.countSewaAsetByStatus([
'PEMBAYARAN DENDA',
'PERIKSA PEMBAYARAN DENDA',
]);
print('[DEBUG] Jumlah sewa diterima: ${diterimaCount.value}');
print('[DEBUG] Jumlah tagihan aktif: ${tagihanAktifCount.value}');
print('[DEBUG] Jumlah denda aktif: ${dendaAktifCount.value}');
}
Future<void> loadActiveRentals() async {
try {
activeRentals.clear();
final sewaAsetList = await _authProvider.getSewaAsetByStatus(['AKTIF']);
for (var sewaAset in sewaAsetList) {
String assetName = 'Aset';
String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) {
final asetData = await _asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) {
assetName = asetData.nama;
imageUrl = asetData.imageUrl;
}
}
DateTime? waktuMulai;
DateTime? waktuSelesai;
String waktuSewa = '';
String tanggalSewa = '';
String jamMulai = '';
String jamSelesai = '';
String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai);
if (namaSatuanWaktu.toLowerCase() == 'jam') {
rentangWaktu = '$jamMulai - $jamSelesai';
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
} else {
rentangWaktu = '$jamMulai - $jamSelesai';
}
waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
}
String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) {
final formatter = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
totalPrice = formatter.format(sewaAset['total']);
}
String duration = '-';
final tagihan = await _asetProvider.getTagihanSewa(sewaAset['id']);
if (tagihan != null) {
final durasiTagihan = tagihan['durasi'] ?? sewaAset['durasi'];
final satuanTagihan = tagihan['nama_satuan_waktu'] ?? namaSatuanWaktu;
duration = '${durasiTagihan ?? '-'} ${satuanTagihan ?? ''}';
} else {
duration = '${sewaAset['durasi'] ?? '-'} ${namaSatuanWaktu ?? ''}';
}
activeRentals.add({
'id': sewaAset['id'] ?? '',
'name': assetName,
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
'waktuSewa': waktuSewa,
'duration': duration,
'status': sewaAset['status'] ?? 'AKTIF',
'totalPrice': totalPrice,
'tanggalSewa': tanggalSewa,
'jamMulai': jamMulai,
'jamSelesai': jamSelesai,
'rentangWaktu': rentangWaktu,
'namaSatuanWaktu': namaSatuanWaktu,
'waktuMulai': sewaAset['waktu_mulai'],
'waktuSelesai': sewaAset['waktu_selesai'],
'can_extend': sewaAset['can_extend'] == true,
});
}
} catch (e) {
print('Error loading active rentals: $e');
}
}
void toSewaAsetTabPaket() {
// Navigasi ke halaman sewa_aset tab Paket (index 1)
Get.toNamed(Routes.SEWA_ASET, arguments: {'tab': 1});
}
Future<void> fetchProfileFromWargaDesa() async {
try {
final user = _authProvider.currentUser;
if (user == null) return;
final userId = user.id;
final data =
await _authProvider.client
.from('warga_desa')
.select('nik, alamat, email, nama_lengkap, no_hp, avatar')
.eq('user_id', userId)
.maybeSingle();
if (data != null) {
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() ?? '';
}
} catch (e) {
print('Error fetching profile from warga_desa: $e');
}
}
}

View File

@ -12,10 +12,10 @@ class WargaSewaController extends GetxController
// Get navigation service
final NavigationService navigationService = Get.find<NavigationService>();
// Get auth provider for user data and sewa_aset queries
final AuthProvider authProvider = Get.find<AuthProvider>();
// Get aset provider for asset data
final AsetProvider asetProvider = Get.find<AsetProvider>();
@ -25,33 +25,35 @@ class WargaSewaController extends GetxController
final acceptedRentals = <Map<String, dynamic>>[].obs;
final completedRentals = <Map<String, dynamic>>[].obs;
final cancelledRentals = <Map<String, dynamic>>[].obs;
final returnedRentals = <Map<String, dynamic>>[].obs;
final activeRentals = <Map<String, dynamic>>[].obs;
// Loading states
final isLoading = false.obs;
final isLoadingPending = false.obs;
final isLoadingAccepted = false.obs;
final isLoadingCompleted = false.obs;
final isLoadingCancelled = false.obs;
final isLoadingReturned = false.obs;
final isLoadingActive = false.obs;
bool _tabSetFromArgument = false;
@override
void onInit() {
super.onInit();
// Ensure tab index is set to Sewa (1)
navigationService.setNavIndex(1);
// Initialize tab controller with 6 tabs
tabController = TabController(length: 6, vsync: this);
// Set initial tab and ensure tab view is updated
tabController.index = 0;
// Initialize tab controller with 7 tabs
tabController = TabController(length: 7, vsync: this);
// Load real rental data for all tabs
loadRentalsData();
loadPendingRentals();
loadAcceptedRentals();
loadActiveRentals();
loadCompletedRentals();
loadCancelledRentals();
loadReturnedRentals();
// Listen to tab changes to update state if needed
tabController.addListener(() {
@ -77,7 +79,9 @@ class WargaSewaController extends GetxController
}
break;
case 3: // Aktif
// Add Aktif tab logic when needed
if (activeRentals.isEmpty && !isLoadingActive.value) {
loadActiveRentals();
}
break;
case 4: // Selesai
if (completedRentals.isEmpty && !isLoadingCompleted.value) {
@ -89,6 +93,11 @@ class WargaSewaController extends GetxController
loadCancelledRentals();
}
break;
case 6: // Dikembalikan
if (returnedRentals.isEmpty && !isLoadingReturned.value) {
loadReturnedRentals();
}
break;
}
});
}
@ -96,9 +105,26 @@ class WargaSewaController extends GetxController
@override
void onReady() {
super.onReady();
// Ensure nav index is set to Sewa (1) when the controller is ready
// This helps maintain correct state during hot reload
navigationService.setNavIndex(1);
// Jalankan update nav index dan tab index setelah build selesai
Future.delayed(Duration.zero, () {
navigationService.setNavIndex(1);
final args = Get.arguments;
int initialTab = 0;
if (!_tabSetFromArgument &&
args != null &&
args is Map &&
args['tab'] != null) {
initialTab =
args['tab'] is int
? args['tab']
: int.tryParse(args['tab'].toString()) ?? 0;
if (tabController.length > initialTab) {
tabController.index = initialTab;
_tabSetFromArgument = true;
}
}
});
}
@override
@ -111,25 +137,25 @@ class WargaSewaController extends GetxController
Future<void> loadRentalsData() async {
try {
isLoading.value = true;
// Clear existing data
rentals.clear();
// Get sewa_aset data with status "MENUNGGU PEMBAYARAN" or "PEMBAYARAN DENDA"
final sewaAsetList = await authProvider.getSewaAsetByStatus([
'MENUNGGU PEMBAYARAN',
'PEMBAYARAN DENDA'
'PEMBAYARAN DENDA',
]);
debugPrint('Fetched ${sewaAsetList.length} sewa_aset records');
// Process each sewa_aset record
for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available
String assetName = 'Aset';
String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) {
@ -137,7 +163,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl;
}
}
// Parse waktu mulai and waktu selesai
DateTime? waktuMulai;
DateTime? waktuSelesai;
@ -146,20 +172,21 @@ class WargaSewaController extends GetxController
String jamMulai = '';
String jamSelesai = '';
String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display
final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day
@ -173,12 +200,13 @@ class WargaSewaController extends GetxController
// Default format
rentangWaktu = '$jamMulai - $jamSelesai';
}
// Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
}
// Format price
String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) {
@ -189,7 +217,7 @@ class WargaSewaController extends GetxController
);
totalPrice = formatter.format(sewaAset['total']);
}
// Add to rentals list
rentals.add({
'id': sewaAset['id'] ?? '',
@ -208,9 +236,10 @@ class WargaSewaController extends GetxController
'namaSatuanWaktu': namaSatuanWaktu,
'waktuMulai': sewaAset['waktu_mulai'],
'waktuSelesai': sewaAset['waktu_selesai'],
'updated_at': sewaAset['updated_at'],
});
}
debugPrint('Processed ${rentals.length} rental records');
} catch (e) {
debugPrint('Error loading rentals data: $e');
@ -245,28 +274,67 @@ class WargaSewaController extends GetxController
}
// Actions
void cancelRental(String id) {
Get.snackbar(
'Info',
'Pembatalan berhasil',
snackPosition: SnackPosition.BOTTOM,
void cancelRental(String id) async {
final confirmed = await Get.dialog<bool>(
AlertDialog(
title: const Text('Konfirmasi Pembatalan'),
content: const Text('Apakah Anda yakin ingin membatalkan pesanan ini?'),
actions: [
TextButton(
onPressed: () => Get.back(result: false),
child: const Text('Tidak'),
),
ElevatedButton(
onPressed: () => Get.back(result: true),
child: const Text('Ya, Batalkan'),
),
],
),
);
if (confirmed == true) {
try {
await asetProvider.client
.from('sewa_aset')
.update({'status': 'DIBATALKAN'})
.eq('id', id);
Get.snackbar(
'Berhasil',
'Pesanan berhasil dibatalkan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
// Refresh data
loadRentalsData();
loadPendingRentals();
loadAcceptedRentals();
loadActiveRentals();
loadCompletedRentals();
loadCancelledRentals();
loadReturnedRentals();
} catch (e) {
Get.snackbar(
'Gagal',
'Gagal membatalkan pesanan: $e',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
}
}
}
// Navigate to payment page with the selected rental data
void viewRentalDetail(Map<String, dynamic> rental) {
debugPrint('Navigating to payment page with rental ID: ${rental['id']}');
// Navigate to payment page with rental data
Get.toNamed(
Routes.PEMBAYARAN_SEWA,
arguments: {
'orderId': rental['id'],
'rentalData': rental,
},
arguments: {'orderId': rental['id'], 'rentalData': rental},
);
}
void payRental(String id) {
Get.snackbar(
'Info',
@ -274,27 +342,27 @@ class WargaSewaController extends GetxController
snackPosition: SnackPosition.BOTTOM,
);
}
// Load data for the Selesai tab (status: SELESAI)
Future<void> loadCompletedRentals() async {
try {
isLoadingCompleted.value = true;
// Clear existing data
completedRentals.clear();
// Get sewa_aset data with status "SELESAI"
final sewaAsetList = await authProvider.getSewaAsetByStatus(['SELESAI']);
debugPrint('Fetched ${sewaAsetList.length} completed sewa_aset records');
// Process each sewa_aset record
for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available
String assetName = 'Aset';
String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) {
@ -302,7 +370,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl;
}
}
// Parse waktu mulai and waktu selesai
DateTime? waktuMulai;
DateTime? waktuSelesai;
@ -311,20 +379,21 @@ class WargaSewaController extends GetxController
String jamMulai = '';
String jamSelesai = '';
String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display
final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day
@ -338,12 +407,13 @@ class WargaSewaController extends GetxController
// Default format
rentangWaktu = '$jamMulai - $jamSelesai';
}
// Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
}
// Format price
String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) {
@ -354,7 +424,7 @@ class WargaSewaController extends GetxController
);
totalPrice = formatter.format(sewaAset['total']);
}
// Add to completed rentals list
completedRentals.add({
'id': sewaAset['id'] ?? '',
@ -374,35 +444,39 @@ class WargaSewaController extends GetxController
'waktuSelesai': sewaAset['waktu_selesai'],
});
}
debugPrint('Processed ${completedRentals.length} completed rental records');
debugPrint(
'Processed ${completedRentals.length} completed rental records',
);
} catch (e) {
debugPrint('Error loading completed rentals data: $e');
} finally {
isLoadingCompleted.value = false;
}
}
// Load data for the Dibatalkan tab (status: DIBATALKAN)
Future<void> loadCancelledRentals() async {
try {
isLoadingCancelled.value = true;
// Clear existing data
cancelledRentals.clear();
// Get sewa_aset data with status "DIBATALKAN"
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DIBATALKAN']);
final sewaAsetList = await authProvider.getSewaAsetByStatus([
'DIBATALKAN',
]);
debugPrint('Fetched ${sewaAsetList.length} cancelled sewa_aset records');
// Process each sewa_aset record
for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available
String assetName = 'Aset';
String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) {
@ -410,7 +484,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl;
}
}
// Parse waktu mulai and waktu selesai
DateTime? waktuMulai;
DateTime? waktuSelesai;
@ -419,20 +493,21 @@ class WargaSewaController extends GetxController
String jamMulai = '';
String jamSelesai = '';
String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display
final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day
@ -446,12 +521,13 @@ class WargaSewaController extends GetxController
// Default format
rentangWaktu = '$jamMulai - $jamSelesai';
}
// Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
}
// Format price
String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) {
@ -462,7 +538,7 @@ class WargaSewaController extends GetxController
);
totalPrice = formatter.format(sewaAset['total']);
}
// Add to cancelled rentals list
cancelledRentals.add({
'id': sewaAset['id'] ?? '',
@ -483,35 +559,40 @@ class WargaSewaController extends GetxController
'alasanPembatalan': sewaAset['alasan_pembatalan'] ?? '-',
});
}
debugPrint('Processed ${cancelledRentals.length} cancelled rental records');
debugPrint(
'Processed ${cancelledRentals.length} cancelled rental records',
);
} catch (e) {
debugPrint('Error loading cancelled rentals data: $e');
} finally {
isLoadingCancelled.value = false;
}
}
// Load data for the Pending tab (status: PERIKSA PEMBAYARAN)
Future<void> loadPendingRentals() async {
try {
isLoadingPending.value = true;
// Clear existing data
pendingRentals.clear();
// Get sewa_aset data with status "PERIKSA PEMBAYARAN"
final sewaAsetList = await authProvider.getSewaAsetByStatus(['PERIKSA PEMBAYARAN']);
// Get sewa_aset data with status 'PERIKSA PEMBAYARAN' dan 'PERIKSA PEMBAYARAN DENDA'
final sewaAsetList = await authProvider.getSewaAsetByStatus([
'PERIKSA PEMBAYARAN',
'PERIKSA PEMBAYARAN DENDA',
]);
debugPrint('Fetched ${sewaAsetList.length} pending sewa_aset records');
// Process each sewa_aset record
for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available
String assetName = 'Aset';
String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) {
@ -519,7 +600,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl;
}
}
// Parse waktu mulai and waktu selesai
DateTime? waktuMulai;
DateTime? waktuSelesai;
@ -528,20 +609,21 @@ class WargaSewaController extends GetxController
String jamMulai = '';
String jamSelesai = '';
String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display
final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day
@ -555,12 +637,13 @@ class WargaSewaController extends GetxController
// Default format
rentangWaktu = '$jamMulai - $jamSelesai';
}
// Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
}
// Format price
String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) {
@ -571,7 +654,7 @@ class WargaSewaController extends GetxController
);
totalPrice = formatter.format(sewaAset['total']);
}
// Add to pending rentals list
pendingRentals.add({
'id': sewaAset['id'] ?? '',
@ -591,7 +674,7 @@ class WargaSewaController extends GetxController
'waktuSelesai': sewaAset['waktu_selesai'],
});
}
debugPrint('Processed ${pendingRentals.length} pending rental records');
} catch (e) {
debugPrint('Error loading pending rentals data: $e');
@ -599,27 +682,27 @@ class WargaSewaController extends GetxController
isLoadingPending.value = false;
}
}
// Load data for the Diterima tab (status: DITERIMA)
Future<void> loadAcceptedRentals() async {
try {
isLoadingAccepted.value = true;
// Clear existing data
acceptedRentals.clear();
// Get sewa_aset data with status "DITERIMA"
final sewaAsetList = await authProvider.getSewaAsetByStatus(['DITERIMA']);
debugPrint('Fetched ${sewaAsetList.length} accepted sewa_aset records');
// Process each sewa_aset record
for (var sewaAset in sewaAsetList) {
// Get asset details if aset_id is available
String assetName = 'Aset';
String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) {
@ -627,7 +710,7 @@ class WargaSewaController extends GetxController
imageUrl = asetData.imageUrl;
}
}
// Parse waktu mulai and waktu selesai
DateTime? waktuMulai;
DateTime? waktuSelesai;
@ -636,20 +719,21 @@ class WargaSewaController extends GetxController
String jamMulai = '';
String jamSelesai = '';
String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
// Format for display
final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai);
// Format based on satuan waktu
if (namaSatuanWaktu.toLowerCase() == 'jam') {
// For hours, show time range on same day
@ -663,12 +747,13 @@ class WargaSewaController extends GetxController
// Default format
rentangWaktu = '$jamMulai - $jamSelesai';
}
// Full time format for waktuSewa
waktuSewa = '${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
}
// Format price
String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) {
@ -679,7 +764,7 @@ class WargaSewaController extends GetxController
);
totalPrice = formatter.format(sewaAset['total']);
}
// Add to accepted rentals list
acceptedRentals.add({
'id': sewaAset['id'] ?? '',
@ -699,7 +784,7 @@ class WargaSewaController extends GetxController
'waktuSelesai': sewaAset['waktu_selesai'],
});
}
debugPrint('Processed ${acceptedRentals.length} accepted rental records');
} catch (e) {
debugPrint('Error loading accepted rentals data: $e');
@ -707,4 +792,166 @@ class WargaSewaController extends GetxController
isLoadingAccepted.value = false;
}
}
Future<void> loadReturnedRentals() async {
try {
isLoadingReturned.value = true;
returnedRentals.clear();
final sewaAsetList = await authProvider.getSewaAsetByStatus([
'DIKEMBALIKAN',
]);
for (var sewaAset in sewaAsetList) {
String assetName = 'Aset';
String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) {
assetName = asetData.nama;
imageUrl = asetData.imageUrl;
}
}
DateTime? waktuMulai;
DateTime? waktuSelesai;
String waktuSewa = '';
String tanggalSewa = '';
String jamMulai = '';
String jamSelesai = '';
String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai);
if (namaSatuanWaktu.toLowerCase() == 'jam') {
rentangWaktu = '$jamMulai - $jamSelesai';
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
} else {
rentangWaktu = '$jamMulai - $jamSelesai';
}
waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
}
String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) {
final formatter = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
totalPrice = formatter.format(sewaAset['total']);
}
returnedRentals.add({
'id': sewaAset['id'] ?? '',
'name': assetName,
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
'waktuSewa': waktuSewa,
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
'status': sewaAset['status'] ?? 'DIKEMBALIKAN',
'totalPrice': totalPrice,
'tanggalSewa': tanggalSewa,
'jamMulai': jamMulai,
'jamSelesai': jamSelesai,
'rentangWaktu': rentangWaktu,
'namaSatuanWaktu': namaSatuanWaktu,
'waktuMulai': sewaAset['waktu_mulai'],
'waktuSelesai': sewaAset['waktu_selesai'],
});
}
} catch (e) {
debugPrint('Error loading returned rentals data: $e');
} finally {
isLoadingReturned.value = false;
}
}
Future<void> loadActiveRentals() async {
try {
isLoadingActive.value = true;
activeRentals.clear();
final sewaAsetList = await authProvider.getSewaAsetByStatus(['AKTIF']);
for (var sewaAset in sewaAsetList) {
String assetName = 'Aset';
String? imageUrl;
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
if (sewaAset['aset_id'] != null) {
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
if (asetData != null) {
assetName = asetData.nama;
imageUrl = asetData.imageUrl;
}
}
DateTime? waktuMulai;
DateTime? waktuSelesai;
String waktuSewa = '';
String tanggalSewa = '';
String jamMulai = '';
String jamSelesai = '';
String rentangWaktu = '';
if (sewaAset['waktu_mulai'] != null &&
sewaAset['waktu_selesai'] != null) {
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
final formatTanggal = DateFormat('dd-MM-yyyy');
final formatWaktu = DateFormat('HH:mm');
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
jamMulai = formatWaktu.format(waktuMulai);
jamSelesai = formatWaktu.format(waktuSelesai);
if (namaSatuanWaktu.toLowerCase() == 'jam') {
rentangWaktu = '$jamMulai - $jamSelesai';
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
} else {
rentangWaktu = '$jamMulai - $jamSelesai';
}
waktuSewa =
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
}
String totalPrice = 'Rp 0';
if (sewaAset['total'] != null) {
final formatter = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
totalPrice = formatter.format(sewaAset['total']);
}
activeRentals.add({
'id': sewaAset['id'] ?? '',
'name': assetName,
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
'waktuSewa': waktuSewa,
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
'status': sewaAset['status'] ?? 'AKTIF',
'totalPrice': totalPrice,
'tanggalSewa': tanggalSewa,
'jamMulai': jamMulai,
'jamSelesai': jamSelesai,
'rentangWaktu': rentangWaktu,
'namaSatuanWaktu': namaSatuanWaktu,
'waktuMulai': sewaAset['waktu_mulai'],
'waktuSelesai': sewaAset['waktu_selesai'],
});
}
} catch (e) {
debugPrint('Error loading active rentals data: $e');
} finally {
isLoadingActive.value = false;
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -117,6 +117,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
),
],
),
dividerColor: Colors.transparent,
labelColor: Colors.white,
unselectedLabelColor: const Color(
0xFF718093,

View File

@ -154,10 +154,10 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
'route': () => controller.navigateToRentals(),
},
{
'title': 'Bayar',
'icon': Icons.payment_outlined,
'title': 'Paket',
'icon': Icons.widgets_outlined,
'color': const Color(0xFF2196F3),
'route': () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
'route': () => controller.toSewaAsetTabPaket(),
},
];
@ -218,32 +218,44 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
child: Column(
children: [
// Sewa Diterima
_buildActivityCard(
title: 'Sewa Diterima',
value: controller.activeRentals.length.toString(),
icon: Icons.check_circle_outline,
color: AppColors.success,
onTap: () => controller.navigateToRentals(),
Obx(
() => _buildActivityCard(
title: 'Sewa Diterima',
value: controller.diterimaCount.value.toString(),
icon: Icons.check_circle_outline,
color: AppColors.success,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 2}),
),
),
const SizedBox(height: 12),
// Tagihan Aktif
_buildActivityCard(
title: 'Tagihan Aktif',
value: controller.activeBills.length.toString(),
icon: Icons.receipt_long_outlined,
color: AppColors.warning,
onTap: () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
Obx(
() => _buildActivityCard(
title: 'Tagihan Aktif',
value: controller.tagihanAktifCount.value.toString(),
icon: Icons.receipt_long_outlined,
color: AppColors.warning,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
),
),
const SizedBox(height: 12),
// Denda Aktif
_buildActivityCard(
title: 'Denda Aktif',
value: controller.activePenalties.length.toString(),
icon: Icons.warning_amber_outlined,
color: AppColors.error,
onTap: () => Get.toNamed(Routes.PEMBAYARAN_SEWA),
Obx(
() => _buildActivityCard(
title: 'Denda Aktif',
value: controller.dendaAktifCount.value.toString(),
icon: Icons.warning_amber_outlined,
color: AppColors.error,
onTap:
() =>
Get.toNamed(Routes.WARGA_SEWA, arguments: {'tab': 0}),
),
),
],
),
@ -357,7 +369,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Sewa Diterima',
'Sewa Aktif',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -498,31 +510,34 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
),
child: Row(
children: [
// Asset icon
// Asset icon/gambar
Container(
padding: const EdgeInsets.all(12),
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
AppColors.primary.withOpacity(0.7),
AppColors.primary,
],
),
borderRadius: BorderRadius.circular(14),
boxShadow: [
BoxShadow(
color: AppColors.primary.withOpacity(0.3),
blurRadius: 8,
offset: const Offset(0, 3),
),
],
color: AppColors.primary.withOpacity(0.08),
),
child: const Icon(
Icons.local_shipping,
color: Colors.white,
size: 24,
child: ClipRRect(
borderRadius: BorderRadius.circular(14),
child:
rental['imageUrl'] != null &&
rental['imageUrl'].toString().isNotEmpty
? Image.network(
rental['imageUrl'],
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) => Icon(
Icons.local_shipping,
color: AppColors.primary,
size: 28,
),
)
: Icon(
Icons.local_shipping,
color: AppColors.primary,
size: 28,
),
),
),
const SizedBox(width: 16),
@ -533,7 +548,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
rental['name'],
rental['name'] ?? '-',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
@ -542,7 +557,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
),
const SizedBox(height: 4),
Text(
rental['time'],
rental['waktuSewa'] ?? '',
style: TextStyle(
fontSize: 13,
color: AppColors.textSecondary,
@ -567,7 +582,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
),
),
child: Text(
rental['price'],
rental['totalPrice'] ?? 'Rp 0',
style: TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
@ -591,14 +606,14 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
child: _buildInfoItem(
icon: Icons.timer_outlined,
title: 'Durasi',
value: rental['duration'],
value: rental['duration'] ?? '-',
),
),
Expanded(
child: _buildInfoItem(
icon: Icons.calendar_today_outlined,
title: 'Status',
value: 'Diterima',
value: rental['status'] ?? '-',
valueColor: AppColors.success,
),
),
@ -608,7 +623,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
const SizedBox(height: 16),
// Action buttons
if (rental['can_extend'])
if ((rental['can_extend'] ?? false) == true)
OutlinedButton.icon(
onPressed: () => controller.extendRental(rental['id']),
icon: const Icon(Icons.update, size: 18),

View File

@ -3,12 +3,16 @@ import 'package:get/get.dart';
import '../controllers/warga_dashboard_controller.dart';
import '../views/warga_layout.dart';
import '../../../theme/app_colors.dart';
import '../../../widgets/app_drawer.dart';
import '../../../services/navigation_service.dart';
class WargaProfileView extends GetView<WargaDashboardController> {
const WargaProfileView({super.key});
@override
Widget build(BuildContext context) {
final navigationService = Get.find<NavigationService>();
navigationService.setNavIndex(2);
return WargaLayout(
appBar: AppBar(
title: const Text('Profil Saya'),
@ -29,6 +33,14 @@ class WargaProfileView extends GetView<WargaDashboardController> {
),
],
),
drawer: AppDrawer(
onNavItemTapped: (index) {
// Handle navigation if needed
},
onLogout: () {
controller.logout();
},
),
backgroundColor: Colors.grey.shade100,
body: RefreshIndicator(
color: AppColors.primary,

File diff suppressed because it is too large Load Diff