first commit

This commit is contained in:
Andreas Malvino
2025-06-02 22:39:03 +07:00
commit e7090af3da
245 changed files with 49210 additions and 0 deletions

View File

@ -0,0 +1,94 @@
import 'package:get/get.dart';
class ListPelangganAktifController extends GetxController {
// Reactive variables
final isLoading = true.obs;
final pelangganList = <Map<String, dynamic>>[].obs;
final searchQuery = ''.obs;
final serviceName = ''.obs;
@override
void onInit() {
super.onInit();
// Get the service name passed from previous page
if (Get.arguments != null && Get.arguments['serviceName'] != null) {
serviceName.value = Get.arguments['serviceName'];
}
// Load the pelanggan data
loadPelangganData();
}
// Load sample pelanggan data (would be replaced with API call in production)
Future<void> loadPelangganData() async {
isLoading.value = true;
try {
// Simulate API call delay
await Future.delayed(const Duration(milliseconds: 800));
// For now, we only have Malih as an active subscriber
final sampleData = [
{
'id': '1',
'nama': 'Malih',
'alamat': 'Jl. Desa Sejahtera No. 15, RT 03/RW 02',
'status': 'Aktif',
'tanggal_mulai': '01/05/2023',
'tanggal_berakhir': '01/05/2024',
'pembayaran_terakhir': '01/04/2024',
'tagihan': 'Rp 20.000',
'telepon': '081234567890',
'email': 'malih@example.com',
'catatan': 'Pelanggan setia sejak 2023',
},
];
pelangganList.assignAll(sampleData);
} catch (e) {
print('Error loading pelanggan data: $e');
} finally {
isLoading.value = false;
}
}
// Filter the list based on search query
List<Map<String, dynamic>> get filteredPelangganList {
if (searchQuery.value.isEmpty) {
return pelangganList;
}
final query = searchQuery.value.toLowerCase();
return pelangganList.where((pelanggan) {
final nama = pelanggan['nama'].toString().toLowerCase();
final alamat = pelanggan['alamat'].toString().toLowerCase();
final status = pelanggan['status'].toString().toLowerCase();
return nama.contains(query) ||
alamat.contains(query) ||
status.contains(query);
}).toList();
}
// Update search query
void updateSearchQuery(String query) {
searchQuery.value = query;
}
// Get status color based on status value
getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'aktif':
return 0xFF4CAF50; // Green
case 'tertunda':
return 0xFFFFA000; // Amber
case 'berakhir':
return 0xFF9E9E9E; // Grey
case 'dibatalkan':
return 0xFFE53935; // Red
default:
return 0xFF2196F3; // Blue
}
}
}

View File

@ -0,0 +1,93 @@
import 'package:get/get.dart';
class ListPetugasMitraController extends GetxController {
// Observable list of partners/mitra
final partners =
<Map<String, dynamic>>[
{
'id': '1',
'name': 'Malih',
'contact': '081234567890',
'address': 'Jl. Desa No. 123, Kecamatan Bumdes, Kabupaten Desa',
'is_active': true,
'role': 'Petugas Lapangan',
'join_date': '10 Januari 2023',
},
].obs;
// Loading state
final isLoading = false.obs;
// Search functionality
final searchQuery = ''.obs;
// Filtered list based on search
List<Map<String, dynamic>> get filteredPartners {
if (searchQuery.value.isEmpty) {
return partners;
}
return partners
.where(
(partner) =>
partner['name'].toString().toLowerCase().contains(
searchQuery.value.toLowerCase(),
) ||
partner['contact'].toString().toLowerCase().contains(
searchQuery.value.toLowerCase(),
) ||
partner['role'].toString().toLowerCase().contains(
searchQuery.value.toLowerCase(),
),
)
.toList();
}
// Add a new partner
void addPartner(Map<String, dynamic> partner) {
partners.add(partner);
Get.back();
Get.snackbar(
'Sukses',
'Petugas mitra berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
);
}
// Edit an existing partner
void editPartner(String id, Map<String, dynamic> updatedPartner) {
final index = partners.indexWhere((partner) => partner['id'] == id);
if (index != -1) {
partners[index] = updatedPartner;
Get.back();
Get.snackbar(
'Sukses',
'Data petugas mitra berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Delete a partner
void deletePartner(String id) {
partners.removeWhere((partner) => partner['id'] == id);
Get.snackbar(
'Sukses',
'Petugas mitra berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
);
}
// Toggle partner active status
void togglePartnerStatus(String id) {
final index = partners.indexWhere((partner) => partner['id'] == id);
if (index != -1) {
final currentStatus = partners[index]['is_active'] as bool;
partners[index]['is_active'] = !currentStatus;
Get.snackbar(
'Status Diperbarui',
'Status petugas mitra diubah menjadi ${!currentStatus ? 'Aktif' : 'Nonaktif'}',
snackPosition: SnackPosition.BOTTOM,
);
}
}
}

View File

@ -0,0 +1,106 @@
import 'package:get/get.dart';
class ListTagihanPeriodeController extends GetxController {
// Reactive variables
final isLoading = true.obs;
final periodeList = <Map<String, dynamic>>[].obs;
final searchQuery = ''.obs;
// Customer data
final pelangganData = Rx<Map<String, dynamic>>({});
final serviceName = ''.obs;
@override
void onInit() {
super.onInit();
// Get the customer data and service name passed from previous page
if (Get.arguments != null) {
if (Get.arguments['pelanggan'] != null) {
pelangganData.value = Map<String, dynamic>.from(
Get.arguments['pelanggan'],
);
}
if (Get.arguments['serviceName'] != null) {
serviceName.value = Get.arguments['serviceName'];
}
}
// Load periode data
loadPeriodeData();
}
// Load sample periode data (would be replaced with API call in production)
Future<void> loadPeriodeData() async {
isLoading.value = true;
try {
// Simulate API call delay
await Future.delayed(const Duration(milliseconds: 800));
// Sample data for periods
final sampleData = [
{
'id': '1',
'bulan': 'Maret',
'tahun': '2025',
'nominal': 'Rp 20.000',
'status_pembayaran': 'Lunas',
'tanggal_pembayaran': '05/03/2025',
'metode_pembayaran': 'Transfer Bank',
'keterangan': 'Pembayaran tepat waktu',
'is_current': true,
},
];
periodeList.assignAll(sampleData);
} catch (e) {
print('Error loading periode data: $e');
} finally {
isLoading.value = false;
}
}
// Filter the list based on search query
List<Map<String, dynamic>> get filteredPeriodeList {
if (searchQuery.value.isEmpty) {
return periodeList;
}
final query = searchQuery.value.toLowerCase();
return periodeList.where((periode) {
final bulan = periode['bulan'].toString().toLowerCase();
final tahun = periode['tahun'].toString().toLowerCase();
final status = periode['status_pembayaran'].toString().toLowerCase();
return bulan.contains(query) ||
tahun.contains(query) ||
status.contains(query);
}).toList();
}
// Update search query
void updateSearchQuery(String query) {
searchQuery.value = query;
}
// Get status color based on payment status
getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'lunas':
return 0xFF4CAF50; // Green
case 'belum lunas':
return 0xFFFFA000; // Amber
case 'terlambat':
return 0xFFE53935; // Red
default:
return 0xFF2196F3; // Blue
}
}
// Get formatted month-year string
String getPeriodeString(Map<String, dynamic> periode) {
return '${periode['bulan']} ${periode['tahun']}';
}
}

View File

@ -0,0 +1,217 @@
import 'package:get/get.dart';
class PetugasAsetController extends GetxController {
// Observable lists for asset data
final asetList = <Map<String, dynamic>>[].obs;
final filteredAsetList = <Map<String, dynamic>>[].obs;
final isLoading = true.obs;
final searchQuery = ''.obs;
// Tab selection (0 for Sewa, 1 for Langganan)
final selectedTabIndex = 0.obs;
// Sort options
final sortBy = 'Nama (A-Z)'.obs;
final sortOptions =
[
'Nama (A-Z)',
'Nama (Z-A)',
'Harga (Rendah-Tinggi)',
'Harga (Tinggi-Rendah)',
].obs;
@override
void onInit() {
super.onInit();
// Load sample data when the controller is initialized
loadAsetData();
}
// Load sample asset data (would be replaced with API call in production)
Future<void> loadAsetData() async {
isLoading.value = true;
try {
// Simulate API call with a delay
await Future.delayed(const Duration(seconds: 1));
// 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,
},
];
asetList.assignAll(sampleData);
applyFilters(); // Apply default filters
} catch (e) {
print('Error loading asset data: $e');
} finally {
isLoading.value = false;
}
}
// Apply filters and sorting to asset list
void applyFilters() {
// Start with all assets
var filtered = List<Map<String, dynamic>>.from(asetList);
// Filter by tab selection (Sewa or Langganan)
String jenisFilter = selectedTabIndex.value == 0 ? 'Sewa' : 'Langganan';
filtered = filtered.where((aset) => aset['jenis'] == jenisFilter).toList();
// Apply search query
if (searchQuery.value.isNotEmpty) {
final query = searchQuery.value.toLowerCase();
filtered =
filtered.where((aset) {
final nama = aset['nama'].toString().toLowerCase();
final deskripsi = aset['deskripsi'].toString().toLowerCase();
final kategori = aset['kategori'].toString().toLowerCase();
return nama.contains(query) ||
deskripsi.contains(query) ||
kategori.contains(query);
}).toList();
}
// Apply sorting
switch (sortBy.value) {
case 'Nama (A-Z)':
filtered.sort(
(a, b) => a['nama'].toString().compareTo(b['nama'].toString()),
);
break;
case 'Nama (Z-A)':
filtered.sort(
(a, b) => b['nama'].toString().compareTo(a['nama'].toString()),
);
break;
case 'Harga (Rendah-Tinggi)':
filtered.sort((a, b) => a['harga'].compareTo(b['harga']));
break;
case 'Harga (Tinggi-Rendah)':
filtered.sort((a, b) => b['harga'].compareTo(a['harga']));
break;
}
// Update filtered list
filteredAsetList.assignAll(filtered);
}
// Change tab (Sewa or Langganan)
void changeTab(int index) {
selectedTabIndex.value = index;
applyFilters();
}
// Set search query
void setSearchQuery(String query) {
searchQuery.value = query;
applyFilters();
}
// Set sort option
void setSortBy(String option) {
sortBy.value = option;
applyFilters();
}
// Format price to Indonesian Rupiah
String formatPrice(int price) {
return 'Rp${price.toString().replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
}
// Add a new asset
void addAset(Map<String, dynamic> newAset) {
// In a real app, this would be an API call
// For demo, we'll just add to the list
asetList.add(newAset);
applyFilters();
}
// Update an existing asset
void updateAset(String id, Map<String, dynamic> updatedData) {
final index = asetList.indexWhere((aset) => aset['id'] == id);
if (index != -1) {
asetList[index] = updatedData;
applyFilters();
}
}
// Delete an asset
void deleteAset(String id) {
asetList.removeWhere((aset) => aset['id'] == id);
applyFilters();
}
}

View File

@ -0,0 +1,217 @@
import 'package:get/get.dart';
class PetugasBumdesCbpController extends GetxController {
// Observable variables
final isLoading = true.obs;
// Bank account data
final bankAccounts =
<Map<String, dynamic>>[
{
'id': '1',
'bank_name': 'Bank BRI',
'account_number': '1234-5678-9101',
'account_holder': 'BUMDes CBP Sukamaju',
'is_primary': true,
},
{
'id': '2',
'bank_name': 'Bank BNI',
'account_number': '9876-5432-1098',
'account_holder': 'BUMDes CBP Sukamaju',
'is_primary': false,
},
].obs;
// Partners data
final partners =
<Map<String, dynamic>>[
{
'id': '1',
'name': 'UD Maju Jaya',
'contact': '081234567890',
'address': 'Jl. Raya Sukamaju No. 123',
'is_active': true,
},
{
'id': '2',
'name': 'CV Tani Mandiri',
'contact': '087654321098',
'address': 'Jl. Kelapa Dua No. 45',
'is_active': true,
},
{
'id': '3',
'name': 'PT Karya Sejahtera',
'contact': '089876543210',
'address': 'Jl. Industri Blok C No. 7',
'is_active': false,
},
].obs;
@override
void onInit() {
super.onInit();
loadData();
}
Future<void> loadData() async {
try {
isLoading.value = true;
// Simulate API delay
await Future.delayed(const Duration(seconds: 1));
// Data is already loaded in the initialized lists
} catch (e) {
print('Error loading data: $e');
Get.snackbar(
'Error',
'Gagal memuat data. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isLoading.value = false;
}
}
// Bank Account Methods
void setPrimaryBankAccount(String id) {
final index = bankAccounts.indexWhere((account) => account['id'] == id);
if (index != -1) {
// First, set all accounts to non-primary
for (int i = 0; i < bankAccounts.length; i++) {
final account = Map<String, dynamic>.from(bankAccounts[i]);
account['is_primary'] = false;
bankAccounts[i] = account;
}
// Then set the selected account as primary
final account = Map<String, dynamic>.from(bankAccounts[index]);
account['is_primary'] = true;
bankAccounts[index] = account;
Get.snackbar(
'Rekening Utama',
'Rekening ${account['bank_name']} telah dijadikan rekening utama',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void addBankAccount(Map<String, dynamic> account) {
// Generate a new ID (in a real app, this would be from the backend)
account['id'] = (bankAccounts.length + 1).toString();
// By default, new accounts are not primary
account['is_primary'] = false;
bankAccounts.add(account);
Get.back();
Get.snackbar(
'Rekening Ditambahkan',
'Rekening bank baru telah berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
);
}
void updateBankAccount(String id, Map<String, dynamic> updatedAccount) {
final index = bankAccounts.indexWhere((account) => account['id'] == id);
if (index != -1) {
// Preserve the ID and primary status
updatedAccount['id'] = id;
updatedAccount['is_primary'] = bankAccounts[index]['is_primary'];
bankAccounts[index] = updatedAccount;
Get.back();
Get.snackbar(
'Rekening Diperbarui',
'Informasi rekening bank telah berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void deleteBankAccount(String id) {
final index = bankAccounts.indexWhere((account) => account['id'] == id);
if (index != -1) {
// Check if trying to delete the primary account
if (bankAccounts[index]['is_primary'] == true) {
Get.snackbar(
'Tidak Dapat Menghapus',
'Rekening utama tidak dapat dihapus. Silakan atur rekening lain sebagai utama terlebih dahulu.',
snackPosition: SnackPosition.BOTTOM,
);
return;
}
bankAccounts.removeAt(index);
Get.back();
Get.snackbar(
'Rekening Dihapus',
'Rekening bank telah berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Partner Methods
void togglePartnerStatus(String id) {
final index = partners.indexWhere((partner) => partner['id'] == id);
if (index != -1) {
final partner = Map<String, dynamic>.from(partners[index]);
partner['is_active'] = !partner['is_active'];
partners[index] = partner;
Get.snackbar(
'Status Diperbarui',
'Status mitra telah diubah menjadi ${partner['is_active'] ? 'Aktif' : 'Tidak Aktif'}',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void addPartner(Map<String, dynamic> partner) {
// Generate a new ID (in a real app, this would be from the backend)
partner['id'] = (partners.length + 1).toString();
// By default, new partners are active
partner['is_active'] = true;
partners.add(partner);
Get.back();
Get.snackbar(
'Mitra Ditambahkan',
'Mitra baru telah berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
);
}
void updatePartner(String id, Map<String, dynamic> updatedPartner) {
final index = partners.indexWhere((partner) => partner['id'] == id);
if (index != -1) {
// Preserve the ID and active status
updatedPartner['id'] = id;
updatedPartner['is_active'] = partners[index]['is_active'];
partners[index] = updatedPartner;
Get.back();
Get.snackbar(
'Mitra Diperbarui',
'Informasi mitra telah berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
);
}
}
void deletePartner(String id) {
final index = partners.indexWhere((partner) => partner['id'] == id);
if (index != -1) {
partners.removeAt(index);
Get.back();
Get.snackbar(
'Mitra Dihapus',
'Mitra telah berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
);
}
}
}

View File

@ -0,0 +1,147 @@
import 'package:get/get.dart';
import '../../../data/providers/auth_provider.dart';
import '../../../routes/app_routes.dart';
class PetugasBumdesDashboardController extends GetxController {
AuthProvider? _authProvider;
// Reactive variables
final userEmail = ''.obs;
final currentTabIndex = 0.obs;
// Revenue Statistics
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
final totalPendapatanBulanLalu = 'Rp 7.200.000'.obs;
final persentaseKenaikan = '18%'.obs;
final isKenaikanPositif = true.obs;
// Revenue by Category
final pendapatanSewa = 'Rp 5.200.000'.obs;
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
// Status Counters for Sewa Aset
final terlaksanaCount = 5.obs;
final dijadwalkanCount = 1.obs;
final aktifCount = 1.obs;
final dibatalkanCount = 3.obs;
// Additional Sewa Aset Status Counters
final menungguPembayaranCount = 2.obs;
final periksaPembayaranCount = 1.obs;
final diterimaCount = 3.obs;
final pembayaranDendaCount = 1.obs;
final periksaPembayaranDendaCount = 0.obs;
final selesaiCount = 4.obs;
// Status counts for Sewa
final pengajuanSewaCount = 5.obs;
final pemasanganCountSewa = 3.obs;
final sewaAktifCount = 10.obs;
final tagihanAktifCountSewa = 7.obs;
final periksaPembayaranCountSewa = 2.obs;
@override
void onInit() {
super.onInit();
try {
_authProvider = Get.find<AuthProvider>();
userEmail.value = _authProvider?.currentUser?.email ?? 'Tidak ada email';
} 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');
}
// 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');
// }
// }
void changeTab(int index) {
try {
currentTabIndex.value = index;
// Navigate to the appropriate page based on the tab index
switch (index) {
case 0:
// Navigate to Dashboard
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
break;
case 1:
// Navigate to Aset page
navigateToAset();
break;
case 2:
// Navigate to Paket page
navigateToPaket();
break;
case 3:
// Navigate to Sewa page
navigateToSewa();
break;
}
} catch (e) {
print('Error changing tab: $e');
}
}
void navigateToAset() {
try {
Get.offAllNamed(Routes.PETUGAS_ASET);
} catch (e) {
print('Error navigating to Aset: $e');
}
}
void navigateToPaket() {
try {
Get.offAllNamed(Routes.PETUGAS_PAKET);
} catch (e) {
print('Error navigating to Paket: $e');
}
}
void navigateToSewa() {
try {
Get.offAllNamed(Routes.PETUGAS_SEWA);
} catch (e) {
print('Error navigating to Sewa: $e');
}
}
void logout() async {
try {
if (_authProvider != null) {
await _authProvider!.signOut();
}
Get.offAllNamed(Routes.LOGIN);
} catch (e) {
print('Error during logout: $e');
// Still try to navigate to login even if sign out fails
Get.offAllNamed(Routes.LOGIN);
}
}
}

View File

@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PetugasManajemenBumdesController extends GetxController {
// Reactive variables
final RxInt selectedTabIndex = 0.obs;
final RxBool isLoading = false.obs;
// Tab options
final List<String> tabOptions = ['Akun Bank', 'Mitra'];
// Sample data for Bank Accounts
final RxList<Map<String, dynamic>> bankAccounts =
<Map<String, dynamic>>[
{
'bankName': 'Bank BRI',
'accountName': 'BUMDes Sejahtera',
'accountNumber': '123456789',
'isPrimary': true,
},
{
'bankName': 'Bank BNI',
'accountName': 'BUMDes Sejahtera',
'accountNumber': '987654321',
'isPrimary': false,
},
].obs;
// Sample data for Partners
final RxList<Map<String, dynamic>> partners =
<Map<String, dynamic>>[
{
'name': 'CV Maju Jaya',
'email': 'majujaya@example.com',
'phone': '081234567890',
'address': 'Jl. Maju No. 123, Kecamatan Berkah',
'isActive': true,
},
{
'name': 'PT Sentosa',
'email': 'sentosa@example.com',
'phone': '089876543210',
'address': 'Jl. Sentosa No. 456, Kecamatan Damai',
'isActive': false,
},
].obs;
@override
void onInit() {
super.onInit();
loadData();
}
void loadData() {
isLoading.value = true;
// Simulate loading data from API
Future.delayed(const Duration(milliseconds: 500), () {
// Data already loaded with sample data
isLoading.value = false;
});
}
void changeTab(int index) {
selectedTabIndex.value = index;
}
void setPrimaryBankAccount(int index) {
// Set all accounts to non-primary first
for (var i = 0; i < bankAccounts.length; i++) {
bankAccounts[i]['isPrimary'] = false;
}
// Set the selected account as primary
bankAccounts[index]['isPrimary'] = true;
// Force UI refresh
bankAccounts.refresh();
Get.snackbar(
'Sukses',
'Rekening utama berhasil diubah',
snackPosition: SnackPosition.BOTTOM,
);
}
void togglePartnerStatus(int index) {
// Toggle the active status
partners[index]['isActive'] = !partners[index]['isActive'];
// Force UI refresh
partners.refresh();
Get.snackbar(
'Sukses',
'Status mitra berhasil diubah',
snackPosition: SnackPosition.BOTTOM,
);
}
void addBankAccount(Map<String, dynamic> account) {
// Set as primary if it's the first account
if (bankAccounts.isEmpty) {
account['isPrimary'] = true;
} else {
account['isPrimary'] = false;
}
bankAccounts.add(account);
Get.snackbar(
'Sukses',
'Rekening bank berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
);
}
void updateBankAccount(int index, Map<String, dynamic> updatedAccount) {
// Preserve the primary status
updatedAccount['isPrimary'] = bankAccounts[index]['isPrimary'];
bankAccounts[index] = updatedAccount;
bankAccounts.refresh();
Get.snackbar(
'Sukses',
'Rekening bank berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
);
}
void deleteBankAccount(int index) {
// Check if the account to be deleted is primary
final isPrimary = bankAccounts[index]['isPrimary'];
// Remove the account
bankAccounts.removeAt(index);
// If the deleted account was primary and there are other accounts, set the first one as primary
if (isPrimary && bankAccounts.isNotEmpty) {
bankAccounts[0]['isPrimary'] = true;
}
bankAccounts.refresh();
Get.snackbar(
'Sukses',
'Rekening bank berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
);
}
void addPartner(Map<String, dynamic> partner) {
partners.add(partner);
Get.snackbar(
'Sukses',
'Mitra berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
);
}
void updatePartner(int index, Map<String, dynamic> updatedPartner) {
partners[index] = updatedPartner;
partners.refresh();
Get.snackbar(
'Sukses',
'Mitra berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
);
}
void deletePartner(int index) {
partners.removeAt(index);
partners.refresh();
Get.snackbar(
'Sukses',
'Mitra berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
);
}
}

View File

@ -0,0 +1,253 @@
import 'package:get/get.dart';
import 'package:intl/intl.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>[
'Terbaru',
'Terlama',
'Harga Tertinggi',
'Harga Terendah',
'Nama A-Z',
'Nama Z-A',
];
// Data dummy paket
final paketList = <Map<String, dynamic>>[].obs;
final filteredPaketList = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
loadPaketData();
}
// Format harga ke Rupiah
String formatPrice(int price) {
final formatter = NumberFormat.currency(
locale: 'id',
symbol: 'Rp ',
decimalDigits: 0,
);
return formatter.format(price);
}
// 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;
}
}
// Set search query dan filter paket
void setSearchQuery(String query) {
searchQuery.value = query;
filterPaket();
}
// Set kategori dan filter paket
void setCategory(String category) {
selectedCategory.value = category;
filterPaket();
}
// Set opsi pengurutan dan filter paket
void setSortBy(String option) {
sortBy.value = option;
sortFilteredList();
}
// 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;
filterPaket();
Get.back();
Get.snackbar(
'Sukses',
'Paket berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
);
}
}
// Hapus paket
void deletePaket(String id) {
paketList.removeWhere((element) => element['id'] == id);
filterPaket();
Get.snackbar(
'Sukses',
'Paket berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
);
}
}

View File

@ -0,0 +1,314 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PetugasSewaController extends GetxController {
// Reactive variables
final isLoading = true.obs;
final searchQuery = ''.obs;
final orderIdQuery = ''.obs;
final selectedStatusFilter = 'Semua'.obs;
final filteredSewaList = <Map<String, dynamic>>[].obs;
// Filter options
final List<String> statusFilters = [
'Semua',
'Menunggu Pembayaran',
'Periksa Pembayaran',
'Diterima',
'Dikembalikan',
'Selesai',
'Dibatalkan',
];
// Mock data for sewa list
final RxList<Map<String, dynamic>> sewaList = <Map<String, dynamic>>[].obs;
@override
void onInit() {
super.onInit();
// Add listeners to update filtered list when any filter changes
ever(searchQuery, (_) => _updateFilteredList());
ever(orderIdQuery, (_) => _updateFilteredList());
ever(selectedStatusFilter, (_) => _updateFilteredList());
ever(sewaList, (_) => _updateFilteredList());
// Load initial data
loadSewaData();
}
// Update filtered list based on current filters
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(),
);
// Apply status filter if not 'Semua'
final matchesStatus =
selectedStatusFilter.value == 'Semua' ||
sewa['status'] == selectedStatusFilter.value;
return matchesSearch && matchesOrderId && matchesStatus;
}).toList();
}
// Load sewa data (mock data for now)
Future<void> loadSewaData() async {
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',
},
]);
} catch (e) {
print('Error loading sewa data: $e');
} finally {
isLoading.value = false;
}
}
// Update search query
void setSearchQuery(String query) {
searchQuery.value = query;
}
// Update order ID query
void setOrderIdQuery(String query) {
orderIdQuery.value = query;
}
// Update status filter
void setStatusFilter(String status) {
selectedStatusFilter.value = status;
applyFilters();
}
void resetFilters() {
selectedStatusFilter.value = 'Semua';
searchQuery.value = '';
filteredSewaList.value = sewaList;
}
void applyFilters() {
filteredSewaList.value =
sewaList.where((sewa) {
bool matchesStatus =
selectedStatusFilter.value == 'Semua' ||
sewa['status'] == selectedStatusFilter.value;
bool matchesSearch =
searchQuery.value.isEmpty ||
sewa['nama_warga'].toLowerCase().contains(
searchQuery.value.toLowerCase(),
);
return matchesStatus && matchesSearch;
}).toList();
}
// Format price to rupiah
String formatPrice(num price) {
return 'Rp ${price.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
}
// 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':
return Colors.green;
case 'Selesai':
return Colors.purple;
case 'Dibatalkan':
return Colors.red;
default:
return Colors.grey;
}
}
// Handle sewa approval (from "Periksa Pembayaran" to "Diterima")
void approveSewa(String 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';
}
sewaList[index] = sewa;
sewaList.refresh();
}
}
// Handle sewa rejection or cancellation
void rejectSewa(String 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;
sewaList.refresh();
}
}
// Request payment for penalty
void requestPenaltyPayment(String 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;
sewaList.refresh();
}
}
// Mark penalty payment as requiring inspection
void markPenaltyForInspection(String 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;
sewaList.refresh();
}
}
// Handle sewa completion
void completeSewa(String id) {
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;
sewaList.refresh();
}
}
// Mark rental as returned
void markAsReturned(String id) {
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;
sewaList.refresh();
}
}
}

View File

@ -0,0 +1,210 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PetugasTambahAsetController extends GetxController {
// Form controllers
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final quantityController = TextEditingController();
final unitOfMeasureController = TextEditingController();
final pricePerHourController = TextEditingController();
final maxHourController = TextEditingController();
final pricePerDayController = TextEditingController();
final maxDayController = TextEditingController();
// Dropdown and toggle values
final selectedCategory = 'Sewa'.obs;
final selectedStatus = 'Tersedia'.obs;
// Replace single selection with multiple selections
final timeOptions = {'Per Jam': true.obs, 'Per Hari': false.obs};
// Category options
final categoryOptions = ['Sewa', 'Langganan'];
final statusOptions = ['Tersedia', 'Pemeliharaan'];
// Images
final selectedImages = <String>[].obs;
// 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() {
// Dispose controllers
nameController.dispose();
descriptionController.dispose();
quantityController.dispose();
unitOfMeasureController.dispose();
pricePerHourController.dispose();
maxHourController.dispose();
pricePerDayController.dispose();
maxDayController.dispose();
super.onClose();
}
// Change selected category
void setCategory(String category) {
selectedCategory.value = category;
validateForm();
}
// Change selected status
void setStatus(String status) {
selectedStatus.value = status;
validateForm();
}
// Toggle time option
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 none selected, force this one to remain selected
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();
}
// Remove image from the list
void removeImage(int index) {
if (index >= 0 && index < selectedImages.length) {
selectedImages.removeAt(index);
validateForm();
}
}
// Validate form fields
void validateForm() {
// Basic validation
bool basicValid =
nameController.text.isNotEmpty &&
descriptionController.text.isNotEmpty &&
quantityController.text.isNotEmpty &&
int.tryParse(quantityController.text) != null;
// Time option validation
bool perHourValid =
!timeOptions['Per Jam']!.value ||
(pricePerHourController.text.isNotEmpty &&
int.tryParse(pricePerHourController.text) != null);
bool perDayValid =
!timeOptions['Per Hari']!.value ||
(pricePerDayController.text.isNotEmpty &&
int.tryParse(pricePerDayController.text) != null);
// At least one time option must be selected
bool anyTimeOptionSelected = false;
timeOptions.forEach((key, value) {
if (value.value) anyTimeOptionSelected = true;
});
isFormValid.value =
basicValid && perHourValid && perDayValid && anyTimeOptionSelected;
}
// Submit form and save 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 = {
'nama': nameController.text,
'deskripsi': descriptionController.text,
'kategori': selectedCategory.value,
'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,
};
// Log the data (in a real app, this would be sent to an API)
print('Asset data: $assetData');
// Return to the asset list page
Get.back();
// Show success message
Get.snackbar(
'Berhasil',
'Aset berhasil ditambahkan',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} catch (e) {
// Show error message
Get.snackbar(
'Gagal',
'Terjadi kesalahan: ${e.toString()}',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isSubmitting.value = false;
}
}
// For demonstration purposes: add sample image
void addSampleImage() {
addImage('assets/images/sample_asset_${selectedImages.length + 1}.jpg');
}
}

View File

@ -0,0 +1,393 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class PetugasTambahPaketController extends GetxController {
// Form controllers
final nameController = TextEditingController();
final descriptionController = TextEditingController();
final priceController = TextEditingController();
final itemQuantityController = TextEditingController();
// Dropdown and toggle values
final selectedCategory = 'Bulanan'.obs;
final selectedStatus = 'Aktif'.obs;
// Category options
final categoryOptions = ['Bulanan', 'Tahunan', 'Premium', 'Bisnis'];
final statusOptions = ['Aktif', 'Nonaktif'];
// Images
final selectedImages = <String>[].obs;
// For package name and description
final packageNameController = TextEditingController();
final packageDescriptionController = TextEditingController();
final packagePriceController = TextEditingController();
// For items/assets in the package
final RxList<Map<String, dynamic>> packageItems =
<Map<String, dynamic>>[].obs;
// For asset selection
final RxList<Map<String, dynamic>> availableAssets =
<Map<String, dynamic>>[].obs;
final Rx<int?> selectedAsset = Rx<int?>(null);
final RxBool isLoadingAssets = false.obs;
// Form validation
final isFormValid = false.obs;
final isSubmitting = false.obs;
@override
void onInit() {
super.onInit();
// Listen to field changes for validation
nameController.addListener(validateForm);
descriptionController.addListener(validateForm);
priceController.addListener(validateForm);
// Load available assets when the controller initializes
fetchAvailableAssets();
}
@override
void onClose() {
// Dispose controllers
nameController.dispose();
descriptionController.dispose();
priceController.dispose();
itemQuantityController.dispose();
packageNameController.dispose();
packageDescriptionController.dispose();
packagePriceController.dispose();
super.onClose();
}
// Change selected category
void setCategory(String category) {
selectedCategory.value = category;
validateForm();
}
// Change selected status
void setStatus(String status) {
selectedStatus.value = status;
validateForm();
}
// Add image to the list (in a real app, this would handle file upload)
void addImage(String imagePath) {
selectedImages.add(imagePath);
validateForm();
}
// Remove image from the list
void removeImage(int index) {
if (index >= 0 && index < selectedImages.length) {
selectedImages.removeAt(index);
validateForm();
}
}
// Fetch available assets from the API or local data
void fetchAvailableAssets() {
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},
];
isLoadingAssets.value = false;
});
}
// Set the selected asset
void setSelectedAsset(int? assetId) {
selectedAsset.value = assetId;
}
// Get remaining stock for an asset (considering current selections)
int getRemainingStock(int assetId) {
// Find the asset in available assets
final asset = availableAssets.firstWhere(
(item) => item['id'] == assetId,
orElse: () => <String, dynamic>{},
);
if (asset.isEmpty) return 0;
// Get total stock
final totalStock = asset['stok'] as int;
// Calculate how many of this asset are already in the package
int alreadySelected = 0;
for (var item in packageItems) {
if (item['asetId'] == assetId) {
alreadySelected += item['jumlah'] as int;
}
}
// Return the remaining available stock
return totalStock - alreadySelected;
}
// Add an asset to the package
void addAssetToPackage() {
if (selectedAsset.value == null || itemQuantityController.text.isEmpty) {
Get.snackbar(
'Error',
'Pilih aset dan masukkan jumlah',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
// Find the selected asset
final asset = availableAssets.firstWhere(
(item) => item['id'] == selectedAsset.value,
orElse: () => <String, dynamic>{},
);
if (asset.isEmpty) return;
// Convert quantity to int
final quantity = int.tryParse(itemQuantityController.text) ?? 0;
if (quantity <= 0) {
Get.snackbar(
'Error',
'Jumlah harus lebih dari 0',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
// Check if quantity is within limits
final remainingStock = getRemainingStock(selectedAsset.value!);
if (quantity > remainingStock) {
Get.snackbar(
'Error',
'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
// Add the item to package
packageItems.add({
'asetId': selectedAsset.value,
'nama': asset['nama'],
'jumlah': quantity,
'stok': asset['stok'],
});
// Clear selection
selectedAsset.value = null;
itemQuantityController.clear();
Get.snackbar(
'Sukses',
'Item berhasil ditambahkan ke paket',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
// Update an existing package item
void updatePackageItem(int index) {
if (selectedAsset.value == null || itemQuantityController.text.isEmpty) {
Get.snackbar(
'Error',
'Pilih aset dan masukkan jumlah',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
// Find the selected asset
final asset = availableAssets.firstWhere(
(item) => item['id'] == selectedAsset.value,
orElse: () => <String, dynamic>{},
);
if (asset.isEmpty) return;
// Convert quantity to int
final quantity = int.tryParse(itemQuantityController.text) ?? 0;
if (quantity <= 0) {
Get.snackbar(
'Error',
'Jumlah harus lebih dari 0',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
// If updating the same asset, check remaining stock + current quantity
final currentItem = packageItems[index];
int availableQuantity = asset['stok'] as int;
// If editing the same asset, we need to consider its current quantity
if (currentItem['asetId'] == selectedAsset.value) {
// For the same asset, we can reuse its current quantity
final alreadyUsed = packageItems
.where(
(item) =>
item['asetId'] == selectedAsset.value &&
packageItems.indexOf(item) != index,
)
.fold(0, (sum, item) => sum + (item['jumlah'] as int));
availableQuantity -= alreadyUsed;
if (quantity > availableQuantity) {
Get.snackbar(
'Error',
'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
} else {
// If changing to a different asset, check the new asset's remaining stock
final remainingStock = getRemainingStock(selectedAsset.value!);
if (quantity > remainingStock) {
Get.snackbar(
'Error',
'Jumlah melebihi stok yang tersedia',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
}
// Update the item
packageItems[index] = {
'asetId': selectedAsset.value,
'nama': asset['nama'],
'jumlah': quantity,
'stok': asset['stok'],
};
// Clear selection
selectedAsset.value = null;
itemQuantityController.clear();
Get.snackbar(
'Sukses',
'Item berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}
// Remove an item from the package
void removeItem(int index) {
packageItems.removeAt(index);
Get.snackbar(
'Dihapus',
'Item berhasil dihapus dari paket',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange,
colorText: Colors.white,
);
}
// Validate form fields
void validateForm() {
// Basic validation
bool basicValid =
nameController.text.isNotEmpty &&
descriptionController.text.isNotEmpty &&
priceController.text.isNotEmpty &&
int.tryParse(priceController.text) != null;
// Package should have at least one item
bool hasItems = packageItems.isNotEmpty;
isFormValid.value = basicValid && hasItems;
}
// Submit form and save package
Future<void> savePaket() async {
if (!isFormValid.value) return;
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
// 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,
};
// Log the data (in a real app, this would be sent to an API)
print('Package data: $paketData');
// Return to the package list page
Get.back();
// Show success message
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()}',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
);
} finally {
isSubmitting.value = false;
}
}
// Old sample method (will be replaced)
void addSampleItem() {
packageItems.add({'nama': 'Laptop Dell XPS', 'jumlah': 1});
}
// Method untuk menambahkan gambar sample
void addSampleImage() {
// Menambahkan URL gambar dummy untuk keperluan pengembangan
selectedImages.add('https://example.com/sample_image.jpg');
validateForm();
}
}