fitur petugas
This commit is contained in:
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)}';
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
Reference in New Issue
Block a user