fitur petugas
This commit is contained in:
@ -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();
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user