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

@ -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(